#
cargo/core/compiler/timings/
report.rs

    1//! Render HTML report from timing tracking data.
    2
    3use 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::Unit;
    13use crate::core::compiler::UnitIndex;
    14
    15use super::CompilationSection;
    16use super::UnitData;
    17use super::UnitTime;
    18
    19/// Name of an individual compilation section.
    20#[derive(Clone, Hash, Eq, PartialEq)]
    21pub enum SectionName {
    22    Frontend,
    23    Codegen,
    24    Named(String),
    25    Other,
    26}
    27
    28impl SectionName {
    29    /// Lower case name.
    30    fn name(&self) -> Cow<'static, str> {
    31        match self {
    32            SectionName::Frontend => "frontend".into(),
    33            SectionName::Codegen => "codegen".into(),
    34            SectionName::Named(n) => n.to_lowercase().into(),
    35            SectionName::Other => "other".into(),
    36        }
    37    }
    38
    39    fn capitalized_name(&self) -> String {
    40        // Make the first "letter" uppercase. We could probably just assume ASCII here, but this
    41        // should be Unicode compatible.
    42        fn capitalize(s: &str) -> String {
    43            let first_char = s
    44                .chars()
    45                .next()
    46                .map(|c| c.to_uppercase().to_string())
    47                .unwrap_or_default();
    48            format!("{first_char}{}", s.chars().skip(1).collect::<String>())
    49        }
    50        capitalize(&self.name())
    51    }
    52}
    53
    54impl serde::ser::Serialize for SectionName {
    55    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    56    where
    57        S: serde::Serializer,
    58    {
    59        self.name().serialize(serializer)
    60    }
    61}
    62
    63/// Postprocessed section data that has both start and an end.
    64#[derive(Copy, Clone, serde::Serialize)]
    65pub struct SectionData {
    66    /// Start (relative to the start of the unit)
    67    pub start: f64,
    68    /// End (relative to the start of the unit)
    69    pub end: f64,
    70}
    71
    72impl SectionData {
    73    fn duration(&self) -> f64 {
    74        (self.end - self.start).max(0.0)
    75    }
    76}
    77
    78/// Concurrency tracking information.
    79#[derive(serde::Serialize)]
    80pub struct Concurrency {
    81    /// Time as an offset in seconds from `Timings::start`.
    82    t: f64,
    83    /// Number of units currently running.
    84    active: usize,
    85    /// Number of units that could run, but are waiting for a jobserver token.
    86    waiting: usize,
    87    /// Number of units that are not yet ready, because they are waiting for
    88    /// dependencies to finish.
    89    inactive: usize,
    90}
    91
    92pub struct RenderContext<'a> {
    93    /// A rendered string of when compilation started.
    94    pub start_str: String,
    95    /// A summary of the root units.
    96    ///
    97    /// Tuples of `(package_description, target_descriptions)`.
    98    pub root_units: Vec<(String, Vec<String>)>,
    99    /// The build profile.
    100    pub profile: String,
    101    /// Total number of fresh units.
    102    pub total_fresh: u32,
    103    /// Total number of dirty units.
    104    pub total_dirty: u32,
    105    /// Time tracking for each individual unit.
    106    pub unit_data: Vec<UnitData>,
    107    /// Concurrency-tracking information. This is periodically updated while
    108    /// compilation progresses.
    109    pub concurrency: Vec<Concurrency>,
    110    /// Recorded CPU states, stored as tuples. First element is when the
    111    /// recording was taken and second element is percentage usage of the
    112    /// system.
    113    pub cpu_usage: &'a [(f64, f64)],
    114    /// Compiler version info, i.e., `rustc 1.92.0-beta.2 (0a411606e 2025-10-31)`.
    115    pub rustc_version: String,
    116    /// The host triple (arch-platform-OS).
    117    pub host: String,
    118    /// The requested target platforms of compilation for this build.
    119    pub requested_targets: Vec<String>,
    120    /// The number of jobs specified for this build.
    121    pub jobs: u32,
    122    /// Available parallelism of the compilation environment.
    123    pub num_cpus: Option<u64>,
    124    /// Fatal error during the build.
    125    pub error: &'a Option<anyhow::Error>,
    126}
    127
    128/// Writes an HTML report.
    129pub fn write_html(ctx: RenderContext<'_>, f: &mut impl Write) -> CargoResult<()> {
    130    // The last concurrency record should equal to the last unit finished time.
    131    let duration = ctx.concurrency.last().map(|c| c.t).unwrap_or(0.0);
    132    let roots: Vec<&str> = ctx
    133        .root_units
    134        .iter()
    135        .map(|(name, _targets)| name.as_str())
    136        .collect();
    137    f.write_all(HTML_TMPL.replace("{ROOTS}", &roots.join(", ")).as_bytes())?;
    138    write_summary_table(&ctx, f, duration)?;
    139    f.write_all(HTML_CANVAS.as_bytes())?;
    140    write_unit_table(&ctx, f)?;
    141    // It helps with pixel alignment to use whole numbers.
    142    writeln!(
    143        f,
    144        "<script>\n\
    145         DURATION = {};",
    146        f64::ceil(duration) as u32
    147    )?;
    148    write_js_data(&ctx, f)?;
    149    write!(
    150        f,
    151        "{}\n\
    152         </script>\n\
    153         </body>\n\
    154         </html>\n\
    155         ",
    156        include_str!("timings.js")
    157    )?;
    158
    159    Ok(())
    160}
    161
    162/// Render the summary table.
    163fn write_summary_table(
    164    ctx: &RenderContext<'_>,
    165    f: &mut impl Write,
    166    duration: f64,
    167) -> CargoResult<()> {
    168    let targets = ctx
    169        .root_units
    170        .iter()
    171        .map(|(name, targets)| format!("{} ({})", name, targets.join(", ")))
    172        .collect::<Vec<_>>()
    173        .join("<br>");
    174
    175    let total_units = ctx.total_fresh + ctx.total_dirty;
    176
    177    let time_human = if duration > 60.0 {
    178        format!(" ({}m {:.1}s)", duration as u32 / 60, duration % 60.0)
    179    } else {
    180        "".to_string()
    181    };
    182    let total_time = format!("{:.1}s{}", duration, time_human);
    183
    184    let max_concurrency = ctx.concurrency.iter().map(|c| c.active).max().unwrap_or(0);
    185    let num_cpus = ctx
    186        .num_cpus
    187        .map(|x| x.to_string())
    188        .unwrap_or_else(|| "n/a".into());
    189
    190    let requested_targets = ctx.requested_targets.join(", ");
    191
    192    let error_msg = match ctx.error {
    193        Some(e) => format!(r#"<tr><td class="error-text">Error:</td><td>{e}</td></tr>"#),
    194        None => "".to_string(),
    195    };
    196
    197    let RenderContext {
    198        start_str,
    199        profile,
    200        total_fresh,
    201        total_dirty,
    202        rustc_version,
    203        host,
    204        jobs,
    205        ..
    206    } = &ctx;
    207
    208    write!(
    209        f,
    210        r#"
    211<table class="my-table summary-table">
    212<tr>
    213<td>Targets:</td><td>{targets}</td>
    214</tr>
    215<tr>
    216<td>Profile:</td><td>{profile}</td>
    217</tr>
    218<tr>
    219<td>Fresh units:</td><td>{total_fresh}</td>
    220</tr>
    221<tr>
    222<td>Dirty units:</td><td>{total_dirty}</td>
    223</tr>
    224<tr>
    225<td>Total units:</td><td>{total_units}</td>
    226</tr>
    227<tr>
    228<td>Max concurrency:</td><td>{max_concurrency} (jobs={jobs} ncpu={num_cpus})</td>
    229</tr>
    230<tr>
    231<td>Build start:</td><td>{start_str}</td>
    232</tr>
    233<tr>
    234<td>Total time:</td><td>{total_time}</td>
    235</tr>
    236<tr>
    237<td>rustc:</td><td>{rustc_version}<br>Host: {host}<br>Target: {requested_targets}</td>
    238</tr>
    239{error_msg}
    240</table>
    241"#,
    242    )?;
    243    Ok(())
    244}
    245
    246/// Write timing data in JavaScript. Primarily for `timings.js` to put data
    247/// in a `<script>` HTML element to draw graphs.
    248fn write_js_data(ctx: &RenderContext<'_>, f: &mut impl Write) -> CargoResult<()> {
    249    writeln!(
    250        f,
    251        "const UNIT_DATA = {};",
    252        serde_json::to_string_pretty(&ctx.unit_data)?
    253    )?;
    254    writeln!(
    255        f,
    256        "const CONCURRENCY_DATA = {};",
    257        serde_json::to_string_pretty(&ctx.concurrency)?
    258    )?;
    259    writeln!(
    260        f,
    261        "const CPU_USAGE = {};",
    262        serde_json::to_string_pretty(&ctx.cpu_usage)?
    263    )?;
    264    Ok(())
    265}
    266
    267/// Render the table of all units.
    268fn write_unit_table(ctx: &RenderContext<'_>, f: &mut impl Write) -> CargoResult<()> {
    269    let mut units: Vec<_> = ctx.unit_data.iter().collect();
    270    units.sort_unstable_by(|a, b| b.duration.partial_cmp(&a.duration).unwrap());
    271
    272    let aggregated: Vec<Option<_>> = units.iter().map(|u| u.sections.as_ref()).collect();
    273
    274    let headers: Vec<_> = aggregated
    275        .iter()
    276        .find_map(|s| s.as_ref())
    277        .map(|sections| {
    278            sections
    279                .iter()
    280                // We don't want to show the "Other" section in the table,
    281                // as it is usually a tiny portion out of the entire unit.
    282                .filter(|(name, _)| !matches!(name, SectionName::Other))
    283                .map(|s| s.0.clone())
    284                .collect()
    285        })
    286        .unwrap_or_default();
    287
    288    write!(
    289        f,
    290        r#"
    291<table class="my-table">
    292<thead>
    293<tr>
    294  <th></th>
    295  <th>Unit</th>
    296  <th>Total</th>
    297  {headers}
    298  <th>Features</th>
    299</tr>
    300</thead>
    301<tbody>
    302"#,
    303        headers = headers
    304            .iter()
    305            .map(|h| format!("<th>{}</th>", h.capitalized_name()))
    306            .join("\n")
    307    )?;
    308
    309    for (i, (unit, aggregated_sections)) in units.iter().zip(aggregated).enumerate() {
    310        let format_duration = |section: Option<&SectionData>| match section {
    311            Some(section) => {
    312                let duration = section.duration();
    313                let pct = (duration / unit.duration) * 100.0;
    314                format!("{duration:.1}s ({:.0}%)", pct)
    315            }
    316            None => "".to_string(),
    317        };
    318
    319        // This is a bit complex, as we assume the most general option - we can have an
    320        // arbitrary set of headers, and an arbitrary set of sections per unit, so we always
    321        // initiate the cells to be empty, and then try to find a corresponding column for which
    322        // we might have data.
    323        let mut cells: HashMap<_, _> = aggregated_sections
    324            .iter()
    325            .flat_map(|sections| sections.into_iter().map(|s| (&s.0, &s.1)))
    326            .collect();
    327
    328        let cells = headers
    329            .iter()
    330            .map(|header| format!("<td>{}</td>", format_duration(cells.remove(header))))
    331            .join("\n");
    332
    333        let features = 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            format_args!("{} v{}", unit.name, unit.version),
    347            unit.target,
    348            unit.duration,
    349        )?;
    350    }
    351    write!(f, "</tbody>\n</table>\n")?;
    352    Ok(())
    353}
    354
    355pub(super) fn to_unit_data(
    356    unit_times: &[UnitTime],
    357    unit_map: &HashMap<Unit, UnitIndex>,
    358) -> Vec<UnitData> {
    359    unit_times
    360        .iter()
    361        .map(|ut| (unit_map[&ut.unit], ut))
    362        .map(|(i, ut)| {
    363            let mode = if ut.unit.mode.is_run_custom_build() {
    364                "run-custom-build"
    365            } else {
    366                "todo"
    367            }
    368            .to_string();
    369            // These filter on the unlocked units because not all unlocked
    370            // units are actually "built". For example, Doctest mode units
    371            // don't actually generate artifacts.
    372            let unblocked_units = ut
    373                .unblocked_units
    374                .iter()
    375                .filter_map(|unit| unit_map.get(unit).copied())
    376                .collect();
    377            let unblocked_rmeta_units = ut
    378                .unblocked_rmeta_units
    379                .iter()
    380                .filter_map(|unit| unit_map.get(unit).copied())
    381                .collect();
    382            let sections = aggregate_sections(ut.sections.clone(), ut.duration, ut.rmeta_time);
    383
    384            UnitData {
    385                i,
    386                name: ut.unit.pkg.name().to_string(),
    387                version: ut.unit.pkg.version().to_string(),
    388                mode,
    389                target: ut.target.clone(),
    390                features: ut.unit.features.iter().map(|f| f.to_string()).collect(),
    391                start: round_to_centisecond(ut.start),
    392                duration: round_to_centisecond(ut.duration),
    393                unblocked_units,
    394                unblocked_rmeta_units,
    395                sections,
    396            }
    397        })
    398        .collect()
    399}
    400
    401/// Derives concurrency information from unit timing data.
    402pub fn compute_concurrency(unit_data: &[UnitData]) -> Vec<Concurrency> {
    403    if unit_data.is_empty() {
    404        return Vec::new();
    405    }
    406
    407    let unit_by_index: HashMap<_, _> = unit_data.iter().map(|u| (u.i, u)).collect();
    408
    409    enum UnblockedBy {
    410        Rmeta(UnitIndex),
    411        Full(UnitIndex),
    412    }
    413
    414    // unit_id -> unit that unblocks it.
    415    let mut unblocked_by: HashMap<_, _> = HashMap::new();
    416    for unit in unit_data {
    417        for id in unit.unblocked_rmeta_units.iter() {
    418            assert!(
    419                unblocked_by
    420                    .insert(*id, UnblockedBy::Rmeta(unit.i))
    421                    .is_none()
    422            );
    423        }
    424
    425        for id in unit.unblocked_units.iter() {
    426            assert!(
    427                unblocked_by
    428                    .insert(*id, UnblockedBy::Full(unit.i))
    429                    .is_none()
    430            );
    431        }
    432    }
    433
    434    let ready_time = |unit: &UnitData| -> Option<f64> {
    435        let dep = unblocked_by.get(&unit.i)?;
    436        match dep {
    437            UnblockedBy::Rmeta(id) => {
    438                let dep = unit_by_index.get(id)?;
    439                let duration = dep.sections.iter().flatten().find_map(|(name, section)| {
    440                    matches!(name, SectionName::Frontend).then_some(section.end)
    441                });
    442
    443                Some(dep.start + duration.unwrap_or(dep.duration))
    444            }
    445            UnblockedBy::Full(id) => {
    446                let dep = unit_by_index.get(id)?;
    447                Some(dep.start + dep.duration)
    448            }
    449        }
    450    };
    451
    452    #[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)]
    453    enum State {
    454        Ready,
    455        Start,
    456        End,
    457    }
    458
    459    let mut events: Vec<_> = unit_data
    460        .iter()
    461        .flat_map(|unit| {
    462            // Adding rounded numbers may cause ready > start,
    463            // so cap with unit.start here to be defensive.
    464            let ready = ready_time(unit).unwrap_or(unit.start).min(unit.start);
    465
    466            [
    467                (ready, State::Ready, unit.i),
    468                (unit.start, State::Start, unit.i),
    469                (unit.start + unit.duration, State::End, unit.i),
    470            ]
    471        })
    472        .collect();
    473
    474    events.sort_by(|a, b| {
    475        a.0.partial_cmp(&b.0)
    476            .unwrap()
    477            .then_with(|| a.1.cmp(&b.1))
    478            .then_with(|| a.2.cmp(&b.2))
    479    });
    480
    481    let mut concurrency: Vec<Concurrency> = Vec::new();
    482    let mut inactive: HashSet<UnitIndex> = unit_data.iter().map(|unit| unit.i).collect();
    483    let mut waiting: HashSet<UnitIndex> = HashSet::new();
    484    let mut active: HashSet<UnitIndex> = HashSet::new();
    485
    486    for (t, state, unit_id) in events {
    487        match state {
    488            State::Ready => {
    489                inactive.remove(&unit_id);
    490                waiting.insert(unit_id);
    491                active.remove(&unit_id);
    492            }
    493            State::Start => {
    494                inactive.remove(&unit_id);
    495                waiting.remove(&unit_id);
    496                active.insert(unit_id);
    497            }
    498            State::End => {
    499                inactive.remove(&unit_id);
    500                waiting.remove(&unit_id);
    501                active.remove(&unit_id);
    502            }
    503        }
    504
    505        let record = Concurrency {
    506            t,
    507            active: active.len(),
    508            waiting: waiting.len(),
    509            inactive: inactive.len(),
    510        };
    511
    512        if let Some(last) = concurrency.last_mut()
    513            && last.t == t
    514        {
    515            // We don't want to draw long vertical lines at the same timestamp,
    516            // so we keep only the latest state.
    517            *last = record;
    518        } else {
    519            concurrency.push(record);
    520        }
    521    }
    522
    523    concurrency
    524}
    525
    526/// Aggregates section timing information from individual compilation sections.
    527///
    528/// We can have a bunch of situations here.
    529///
    530/// - `-Zsection-timings` is enabled, and we received some custom sections,
    531///   in which case we use them to determine the headers.
    532/// - We have at least one rmeta time, so we hard-code Frontend and Codegen headers.
    533/// - We only have total durations, so we don't add any additional headers.
    534pub fn aggregate_sections(
    535    sections: IndexMap<String, CompilationSection>,
    536    end: f64,
    537    rmeta_time: Option<f64>,
    538) -> Option<Vec<(SectionName, SectionData)>> {
    539    if !sections.is_empty() {
    540        // We have some detailed compilation section timings, so we postprocess them
    541        // Since it is possible that we do not have an end timestamp for a given compilation
    542        // section, we need to iterate them and if an end is missing, we assign the end of
    543        // the section to the start of the following section.
    544        let mut sections = sections.into_iter().fold(
    545            // The frontend section is currently implicit in rustc.
    546            // It is assumed to start at compilation start and end when codegen starts,
    547            // So we hard-code it here.
    548            vec![(
    549                SectionName::Frontend,
    550                SectionData {
    551                    start: 0.0,
    552                    end: round_to_centisecond(end),
    553                },
    554            )],
    555            |mut sections, (name, section)| {
    556                let previous = sections.last_mut().unwrap();
    557                // Setting the end of previous to the start of the current.
    558                previous.1.end = section.start;
    559
    560                sections.push((
    561                    SectionName::Named(name),
    562                    SectionData {
    563                        start: round_to_centisecond(section.start),
    564                        end: round_to_centisecond(section.end.unwrap_or(end)),
    565                    },
    566                ));
    567
    568                sections
    569            },
    570        );
    571
    572        // We draw the sections in the pipeline graph in a way where the frontend
    573        // section has the "default" build color, and then additional sections
    574        // (codegen, link) are overlaid on top with a different color.
    575        // However, there might be some time after the final (usually link) section,
    576        // which definitely shouldn't be classified as "Frontend". We thus try to
    577        // detect this situation and add a final "Other" section.
    578        if let Some((_, section)) = sections.last()
    579            && section.end < end
    580        {
    581            sections.push((
    582                SectionName::Other,
    583                SectionData {
    584                    start: round_to_centisecond(section.end),
    585                    end: round_to_centisecond(end),
    586                },
    587            ));
    588        }
    589        Some(sections)
    590    } else if let Some(rmeta) = rmeta_time {
    591        // We only know when the rmeta time was generated
    592        Some(vec![
    593            (
    594                SectionName::Frontend,
    595                SectionData {
    596                    start: 0.0,
    597                    end: round_to_centisecond(rmeta),
    598                },
    599            ),
    600            (
    601                SectionName::Codegen,
    602                SectionData {
    603                    start: round_to_centisecond(rmeta),
    604                    end: round_to_centisecond(end),
    605                },
    606            ),
    607        ])
    608    } else {
    609        // No section data provided. We only know the total duration.
    610        None
    611    }
    612}
    613
    614/// Rounds seconds to 0.01s precision.
    615pub fn round_to_centisecond(x: f64) -> f64 {
    616    (x * 100.0).round() / 100.0
    617}
    618
    619static HTML_TMPL: &str = r#"
    620<html>
    621<head>
    622  <title>Cargo Build Timings — {ROOTS}</title>
    623  <meta charset="utf-8">
    624<style type="text/css">
    625:root {
    626  --error-text: #e80000;
    627  --text: #000;
    628  --background: #fff;
    629  --h1-border-bottom: #c0c0c0;
    630  --table-box-shadow: rgba(0, 0, 0, 0.1);
    631  --table-th: #d5dde5;
    632  --table-th-background: #1b1e24;
    633  --table-th-border-bottom: #9ea7af;
    634  --table-th-border-right: #343a45;
    635  --table-tr-border-top: #c1c3d1;
    636  --table-tr-border-bottom: #c1c3d1;
    637  --table-tr-odd-background: #ebebeb;
    638  --table-td-background: #ffffff;
    639  --table-td-border-right: #C1C3D1;
    640  --canvas-background: #f7f7f7;
    641  --canvas-axes: #303030;
    642  --canvas-grid: #e6e6e6;
    643  --canvas-codegen: #aa95e8;
    644  --canvas-link: #95e8aa;
    645  --canvas-other: #e895aa;
    646  --canvas-custom-build: #f0b165;
    647  --canvas-not-custom-build: #95cce8;
    648  --canvas-dep-line: #ddd;
    649  --canvas-dep-line-highlighted: #000;
    650  --canvas-cpu: rgba(250, 119, 0, 0.2);
    651}
    652
    653@media (prefers-color-scheme: dark) {
    654  :root {
    655    --error-text: #e80000;
    656    --text: #fff;
    657    --background: #121212;
    658    --h1-border-bottom: #444;
    659    --table-box-shadow: rgba(255, 255, 255, 0.1);
    660    --table-th: #a0a0a0;
    661    --table-th-background: #2c2c2c;
    662    --table-th-border-bottom: #555;
    663    --table-th-border-right: #444;
    664    --table-tr-border-top: #333;
    665    --table-tr-border-bottom: #333;
    666    --table-tr-odd-background: #1e1e1e;
    667    --table-td-background: #262626;
    668    --table-td-border-right: #333;
    669    --canvas-background: #1a1a1a;
    670    --canvas-axes: #b0b0b0;
    671    --canvas-grid: #333;
    672    --canvas-block: #aa95e8;
    673    --canvas-custom-build: #f0b165;
    674    --canvas-not-custom-build: #95cce8;
    675    --canvas-dep-line: #444;
    676    --canvas-dep-line-highlighted: #fff;
    677    --canvas-cpu: rgba(250, 119, 0, 0.2);
    678  }
    679}
    680
    681html {
    682  font-family: sans-serif;
    683  color: var(--text);
    684  background: var(--background);
    685}
    686
    687.canvas-container {
    688  position: relative;
    689  margin-top: 5px;
    690  margin-bottom: 5px;
    691}
    692
    693.canvas-container.hidden {
    694  display: none;
    695}
    696
    697h1 {
    698  border-bottom: 1px solid var(--h1-border-bottom);
    699}
    700
    701.graph {
    702  display: block;
    703}
    704
    705.my-table {
    706  margin-top: 20px;
    707  margin-bottom: 20px;
    708  border-collapse: collapse;
    709  box-shadow: 0 5px 10px var(--table-box-shadow);
    710}
    711
    712.my-table th {
    713  color: var(--table-th);
    714  background: var(--table-th-background);
    715  border-bottom: 4px solid var(--table-th-border-bottom);
    716  border-right: 1px solid var(--table-th-border-right);
    717  font-size: 18px;
    718  font-weight: 100;
    719  padding: 12px;
    720  text-align: left;
    721  vertical-align: middle;
    722}
    723
    724.my-table th:first-child {
    725  border-top-left-radius: 3px;
    726}
    727
    728.my-table th:last-child {
    729  border-top-right-radius: 3px;
    730  border-right:none;
    731}
    732
    733.my-table tr {
    734  border-top: 1px solid var(--table-tr-border-top);
    735  border-bottom: 1px solid var(--table-tr-border-bottom);
    736  font-size: 16px;
    737  font-weight: normal;
    738}
    739
    740.my-table tr:first-child {
    741  border-top:none;
    742}
    743
    744.my-table tr:last-child {
    745  border-bottom:none;
    746}
    747
    748.my-table tr:nth-child(odd) td {
    749  background: var(--table-tr-odd-background);
    750}
    751
    752.my-table tr:last-child td:first-child {
    753  border-bottom-left-radius:3px;
    754}
    755
    756.my-table tr:last-child td:last-child {
    757  border-bottom-right-radius:3px;
    758}
    759
    760.my-table td {
    761  background: var(--table-td-background);
    762  padding: 10px;
    763  text-align: left;
    764  vertical-align: middle;
    765  font-weight: 300;
    766  font-size: 14px;
    767  border-right: 1px solid var(--table-td-border-right);
    768}
    769
    770.my-table td:last-child {
    771  border-right: 0px;
    772}
    773
    774.summary-table td:first-child {
    775  vertical-align: top;
    776  text-align: right;
    777}
    778
    779.input-table td {
    780  text-align: center;
    781}
    782
    783.error-text {
    784  color: var(--error-text);
    785}
    786
    787</style>
    788</head>
    789<body>
    790
    791<h1>Cargo Build Timings</h1>
    792See <a href="https://doc.rust-lang.org/nightly/cargo/reference/timings.html">Documentation</a>
    793"#;
    794
    795static HTML_CANVAS: &str = r#"
    796<table class="input-table">
    797  <tr>
    798    <td><label for="min-unit-time">Min unit time:</label></td>
    799    <td title="Scale corresponds to a number of pixels per second. It is automatically initialized based on your viewport width.">
    800      <label for="scale">Scale:</label>
    801    </td>
    802    <td>Renderer:</td>
    803  </tr>
    804  <tr>
    805    <td><input type="range" min="0" max="30" step="0.1" value="0" id="min-unit-time"></td>
    806    <!--
    807        The scale corresponds to some number of "pixels per second".
    808        Its min, max, and initial values are automatically set by JavaScript on page load,
    809        based on the client viewport.
    810    -->
    811    <td><input type="range" min="1" max="100" value="50" id="scale"></td>
    812    <td>
    813        <label>
    814            <input type="radio" name="renderer" value="canvas" checked />
    815            Canvas
    816        </label>
    817        <label>
    818            <input type="radio" name="renderer" value="svg" />
    819            SVG
    820        </label>
    821    </td>
    822  </tr>
    823  <tr>
    824    <td><output for="min-unit-time" id="min-unit-time-output"></output></td>
    825    <td><output for="scale" id="scale-output"></output></td>
    826    <td></td>
    827  </tr>
    828</table>
    829
    830<div id="pipeline-container" class="canvas-container" part="canvas">
    831 <canvas id="pipeline-graph" class="graph" style="position: absolute; left: 0; top: 0; z-index: 0;"></canvas>
    832 <canvas id="pipeline-graph-lines" style="position: absolute; left: 0; top: 0; z-index: 1; pointer-events:none;"></canvas>
    833</div>
    834<div class="canvas-container" part="canvas">
    835  <canvas id="timing-graph" class="graph"></canvas>
    836</div>
    837<div id="pipeline-container-svg" class="canvas-container" part="svg"></div>
    838<div id="timing-container-svg" class="canvas-container" part="svg"></div>
    839"#;