#
cargo/core/compiler/
timings.rs

    1//! Timing tracking.
    2//!
    3//! This module implements some simple tracking information for timing of how
    4//! long it takes for different units to compile.
    5use super::{CompileMode, Unit};
    6use crate::core::compiler::job_queue::JobId;
    7use crate::core::compiler::{BuildContext, BuildRunner, TimingOutput};
    8use crate::core::PackageId;
    9use crate::util::cpu::State;
    10use crate::util::machine_message::{self, Message};
    11use crate::util::style;
    12use crate::util::{CargoResult, GlobalContext};
    13use anyhow::Context as _;
    14use cargo_util::paths;
    15use std::collections::HashMap;
    16use std::io::{BufWriter, Write};
    17use std::thread::available_parallelism;
    18use std::time::{Duration, Instant};
    19
    20/// Tracking information for the entire build.
    21///
    22/// Methods on this structure are generally called from the main thread of a
    23/// running [`JobQueue`] instance (`DrainState` in specific) when the queue
    24/// receives messages from spawned off threads.
    25///
    26/// [`JobQueue`]: super::JobQueue
    27pub struct Timings<'gctx> {
    28    gctx: &'gctx GlobalContext,
    29    /// Whether or not timings should be captured.
    30    enabled: bool,
    31    /// If true, saves an HTML report to disk.
    32    report_html: bool,
    33    /// If true, emits JSON information with timing information.
    34    report_json: bool,
    35    /// When Cargo started.
    36    start: Instant,
    37    /// A rendered string of when compilation started.
    38    start_str: String,
    39    /// A summary of the root units.
    40    ///
    41    /// Tuples of `(package_description, target_descriptions)`.
    42    root_targets: Vec<(String, Vec<String>)>,
    43    /// The build profile.
    44    profile: String,
    45    /// Total number of fresh units.
    46    total_fresh: u32,
    47    /// Total number of dirty units.
    48    total_dirty: u32,
    49    /// Time tracking for each individual unit.
    50    unit_times: Vec<UnitTime>,
    51    /// Units that are in the process of being built.
    52    /// When they finished, they are moved to `unit_times`.
    53    active: HashMap<JobId, UnitTime>,
    54    /// Concurrency-tracking information. This is periodically updated while
    55    /// compilation progresses.
    56    concurrency: Vec<Concurrency>,
    57    /// Last recorded state of the system's CPUs and when it happened
    58    last_cpu_state: Option<State>,
    59    last_cpu_recording: Instant,
    60    /// Recorded CPU states, stored as tuples. First element is when the
    61    /// recording was taken and second element is percentage usage of the
    62    /// system.
    63    cpu_usage: Vec<(f64, f64)>,
    64}
    65
    66/// Tracking information for an individual unit.
    67struct UnitTime {
    68    unit: Unit,
    69    /// A string describing the cargo target.
    70    target: String,
    71    /// The time when this unit started as an offset in seconds from `Timings::start`.
    72    start: f64,
    73    /// Total time to build this unit in seconds.
    74    duration: f64,
    75    /// The time when the `.rmeta` file was generated, an offset in seconds
    76    /// from `start`.
    77    rmeta_time: Option<f64>,
    78    /// Reverse deps that are freed to run after this unit finished.
    79    unlocked_units: Vec<Unit>,
    80    /// Same as `unlocked_units`, but unlocked by rmeta.
    81    unlocked_rmeta_units: Vec<Unit>,
    82}
    83
    84/// Periodic concurrency tracking information.
    85#[derive(serde::Serialize)]
    86struct Concurrency {
    87    /// Time as an offset in seconds from `Timings::start`.
    88    t: f64,
    89    /// Number of units currently running.
    90    active: usize,
    91    /// Number of units that could run, but are waiting for a jobserver token.
    92    waiting: usize,
    93    /// Number of units that are not yet ready, because they are waiting for
    94    /// dependencies to finish.
    95    inactive: usize,
    96}
    97
    98impl<'gctx> Timings<'gctx> {
    99    pub fn new(bcx: &BuildContext<'_, 'gctx>, root_units: &[Unit]) -> Timings<'gctx> {
    100        let has_report = |what| bcx.build_config.timing_outputs.contains(&what);
    101        let report_html = has_report(TimingOutput::Html);
    102        let report_json = has_report(TimingOutput::Json);
    103        let enabled = report_html | report_json;
    104
    105        let mut root_map: HashMap<PackageId, Vec<String>> = HashMap::new();
    106        for unit in root_units {
    107            let target_desc = unit.target.description_named();
    108            root_map
    109                .entry(unit.pkg.package_id())
    110                .or_default()
    111                .push(target_desc);
    112        }
    113        let root_targets = root_map
    114            .into_iter()
    115            .map(|(pkg_id, targets)| {
    116                let pkg_desc = format!("{} {}", pkg_id.name(), pkg_id.version());
    117                (pkg_desc, targets)
    118            })
    119            .collect();
    120        let start_str = jiff::Timestamp::now().to_string();
    121        let profile = bcx.build_config.requested_profile.to_string();
    122        let last_cpu_state = if enabled {
    123            match State::current() {
    124                Ok(state) => Some(state),
    125                Err(e) => {
    126                    tracing::info!("failed to get CPU state, CPU tracking disabled: {:?}", e);
    127                    None
    128                }
    129            }
    130        } else {
    131            None
    132        };
    133
    134        Timings {
    135            gctx: bcx.gctx,
    136            enabled,
    137            report_html,
    138            report_json,
    139            start: bcx.gctx.creation_time(),
    140            start_str,
    141            root_targets,
    142            profile,
    143            total_fresh: 0,
    144            total_dirty: 0,
    145            unit_times: Vec::new(),
    146            active: HashMap::new(),
    147            concurrency: Vec::new(),
    148            last_cpu_state,
    149            last_cpu_recording: Instant::now(),
    150            cpu_usage: Vec::new(),
    151        }
    152    }
    153
    154    /// Mark that a unit has started running.
    155    pub fn unit_start(&mut self, id: JobId, unit: Unit) {
    156        if !self.enabled {
    157            return;
    158        }
    159        let mut target = if unit.target.is_lib() && unit.mode == CompileMode::Build {
    160            // Special case for brevity, since most dependencies hit
    161            // this path.
    162            "".to_string()
    163        } else {
    164            format!(" {}", unit.target.description_named())
    165        };
    166        match unit.mode {
    167            CompileMode::Test => target.push_str(" (test)"),
    168            CompileMode::Build => {}
    169            CompileMode::Check { test: true } => target.push_str(" (check-test)"),
    170            CompileMode::Check { test: false } => target.push_str(" (check)"),
    171            CompileMode::Bench => target.push_str(" (bench)"),
    172            CompileMode::Doc { .. } => target.push_str(" (doc)"),
    173            CompileMode::Doctest => target.push_str(" (doc test)"),
    174            CompileMode::Docscrape => target.push_str(" (doc scrape)"),
    175            CompileMode::RunCustomBuild => target.push_str(" (run)"),
    176        }
    177        let unit_time = UnitTime {
    178            unit,
    179            target,
    180            start: self.start.elapsed().as_secs_f64(),
    181            duration: 0.0,
    182            rmeta_time: None,
    183            unlocked_units: Vec::new(),
    184            unlocked_rmeta_units: Vec::new(),
    185        };
    186        assert!(self.active.insert(id, unit_time).is_none());
    187    }
    188
    189    /// Mark that the `.rmeta` file as generated.
    190    pub fn unit_rmeta_finished(&mut self, id: JobId, unlocked: Vec<&Unit>) {
    191        if !self.enabled {
    192            return;
    193        }
    194        // `id` may not always be active. "fresh" units unconditionally
    195        // generate `Message::Finish`, but this active map only tracks dirty
    196        // units.
    197        let Some(unit_time) = self.active.get_mut(&id) else {
    198            return;
    199        };
    200        let t = self.start.elapsed().as_secs_f64();
    201        unit_time.rmeta_time = Some(t - unit_time.start);
    202        assert!(unit_time.unlocked_rmeta_units.is_empty());
    203        unit_time
    204            .unlocked_rmeta_units
    205            .extend(unlocked.iter().cloned().cloned());
    206    }
    207
    208    /// Mark that a unit has finished running.
    209    pub fn unit_finished(&mut self, id: JobId, unlocked: Vec<&Unit>) {
    210        if !self.enabled {
    211            return;
    212        }
    213        // See note above in `unit_rmeta_finished`, this may not always be active.
    214        let Some(mut unit_time) = self.active.remove(&id) else {
    215            return;
    216        };
    217        let t = self.start.elapsed().as_secs_f64();
    218        unit_time.duration = t - unit_time.start;
    219        assert!(unit_time.unlocked_units.is_empty());
    220        unit_time
    221            .unlocked_units
    222            .extend(unlocked.iter().cloned().cloned());
    223        if self.report_json {
    224            let msg = machine_message::TimingInfo {
    225                package_id: unit_time.unit.pkg.package_id().to_spec(),
    226                target: &unit_time.unit.target,
    227                mode: unit_time.unit.mode,
    228                duration: unit_time.duration,
    229                rmeta_time: unit_time.rmeta_time,
    230            }
    231            .to_json_string();
    232            crate::drop_println!(self.gctx, "{}", msg);
    233        }
    234        self.unit_times.push(unit_time);
    235    }
    236
    237    /// This is called periodically to mark the concurrency of internal structures.
    238    pub fn mark_concurrency(&mut self, active: usize, waiting: usize, inactive: usize) {
    239        if !self.enabled {
    240            return;
    241        }
    242        let c = Concurrency {
    243            t: self.start.elapsed().as_secs_f64(),
    244            active,
    245            waiting,
    246            inactive,
    247        };
    248        self.concurrency.push(c);
    249    }
    250
    251    /// Mark that a fresh unit was encountered. (No re-compile needed)
    252    pub fn add_fresh(&mut self) {
    253        self.total_fresh += 1;
    254    }
    255
    256    /// Mark that a dirty unit was encountered. (Re-compile needed)
    257    pub fn add_dirty(&mut self) {
    258        self.total_dirty += 1;
    259    }
    260
    261    /// Take a sample of CPU usage
    262    pub fn record_cpu(&mut self) {
    263        if !self.enabled {
    264            return;
    265        }
    266        let Some(prev) = &mut self.last_cpu_state else {
    267            return;
    268        };
    269        // Don't take samples too frequently, even if requested.
    270        let now = Instant::now();
    271        if self.last_cpu_recording.elapsed() < Duration::from_millis(100) {
    272            return;
    273        }
    274        let current = match State::current() {
    275            Ok(s) => s,
    276            Err(e) => {
    277                tracing::info!("failed to get CPU state: {:?}", e);
    278                return;
    279            }
    280        };
    281        let pct_idle = current.idle_since(prev);
    282        *prev = current;
    283        self.last_cpu_recording = now;
    284        let dur = now.duration_since(self.start).as_secs_f64();
    285        self.cpu_usage.push((dur, 100.0 - pct_idle));
    286    }
    287
    288    /// Call this when all units are finished.
    289    pub fn finished(
    290        &mut self,
    291        build_runner: &BuildRunner<'_, '_>,
    292        error: &Option<anyhow::Error>,
    293    ) -> CargoResult<()> {
    294        if !self.enabled {
    295            return Ok(());
    296        }
    297        self.mark_concurrency(0, 0, 0);
    298        self.unit_times
    299            .sort_unstable_by(|a, b| a.start.partial_cmp(&b.start).unwrap());
    300        if self.report_html {
    301            self.report_html(build_runner, error)
    302                .context("failed to save timing report")?;
    303        }
    304        Ok(())
    305    }
    306
    307    /// Save HTML report to disk.
    308    fn report_html(
    309        &self,
    310        build_runner: &BuildRunner<'_, '_>,
    311        error: &Option<anyhow::Error>,
    312    ) -> CargoResult<()> {
    313        let duration = self.start.elapsed().as_secs_f64();
    314        let timestamp = self.start_str.replace(&['-', ':'][..], "");
    315        let timings_path = build_runner.files().host_root().join("cargo-timings");
    316        paths::create_dir_all(&timings_path)?;
    317        let filename = timings_path.join(format!("cargo-timing-{}.html", timestamp));
    318        let mut f = BufWriter::new(paths::create(&filename)?);
    319        let roots: Vec<&str> = self
    320            .root_targets
    321            .iter()
    322            .map(|(name, _targets)| name.as_str())
    323            .collect();
    324        f.write_all(HTML_TMPL.replace("{ROOTS}", &roots.join(", ")).as_bytes())?;
    325        self.write_summary_table(&mut f, duration, build_runner.bcx, error)?;
    326        f.write_all(HTML_CANVAS.as_bytes())?;
    327        self.write_unit_table(&mut f)?;
    328        // It helps with pixel alignment to use whole numbers.
    329        writeln!(
    330            f,
    331            "<script>\n\
    332             DURATION = {};",
    333            f64::ceil(duration) as u32
    334        )?;
    335        self.write_js_data(&mut f)?;
    336        write!(
    337            f,
    338            "{}\n\
    339             </script>\n\
    340             </body>\n\
    341             </html>\n\
    342             ",
    343            include_str!("timings.js")
    344        )?;
    345        drop(f);
    346
    347        let unstamped_filename = timings_path.join("cargo-timing.html");
    348        paths::link_or_copy(&filename, &unstamped_filename)?;
    349
    350        let mut shell = self.gctx.shell();
    351        let timing_path = std::env::current_dir().unwrap_or_default().join(&filename);
    352        let link = shell.err_file_hyperlink(&timing_path);
    353        let msg = format!("report saved to {link}{}{link:#}", timing_path.display(),);
    354        shell.status_with_color("Timing", msg, &style::NOTE)?;
    355
    356        Ok(())
    357    }
    358
    359    /// Render the summary table.
    360    fn write_summary_table(
    361        &self,
    362        f: &mut impl Write,
    363        duration: f64,
    364        bcx: &BuildContext<'_, '_>,
    365        error: &Option<anyhow::Error>,
    366    ) -> CargoResult<()> {
    367        let targets: Vec<String> = self
    368            .root_targets
    369            .iter()
    370            .map(|(name, targets)| format!("{} ({})", name, targets.join(", ")))
    371            .collect();
    372        let targets = targets.join("<br>");
    373        let time_human = if duration > 60.0 {
    374            format!(" ({}m {:.1}s)", duration as u32 / 60, duration % 60.0)
    375        } else {
    376            "".to_string()
    377        };
    378        let total_time = format!("{:.1}s{}", duration, time_human);
    379        let max_concurrency = self.concurrency.iter().map(|c| c.active).max().unwrap();
    380        let num_cpus = available_parallelism()
    381            .map(|x| x.get().to_string())
    382            .unwrap_or_else(|_| "n/a".into());
    383        let rustc_info = render_rustc_info(bcx);
    384        let error_msg = match error {
    385            Some(e) => format!(r#"<tr><td class="error-text">Error:</td><td>{e}</td></tr>"#),
    386            None => "".to_string(),
    387        };
    388        write!(
    389            f,
    390            r#"
    391<table class="my-table summary-table">
    392  <tr>
    393    <td>Targets:</td><td>{}</td>
    394  </tr>
    395  <tr>
    396    <td>Profile:</td><td>{}</td>
    397  </tr>
    398  <tr>
    399    <td>Fresh units:</td><td>{}</td>
    400  </tr>
    401  <tr>
    402    <td>Dirty units:</td><td>{}</td>
    403  </tr>
    404  <tr>
    405    <td>Total units:</td><td>{}</td>
    406  </tr>
    407  <tr>
    408    <td>Max concurrency:</td><td>{} (jobs={} ncpu={})</td>
    409  </tr>
    410  <tr>
    411    <td>Build start:</td><td>{}</td>
    412  </tr>
    413  <tr>
    414    <td>Total time:</td><td>{}</td>
    415  </tr>
    416  <tr>
    417    <td>rustc:</td><td>{}</td>
    418  </tr>
    419{}
    420</table>
    421"#,
    422            targets,
    423            self.profile,
    424            self.total_fresh,
    425            self.total_dirty,
    426            self.total_fresh + self.total_dirty,
    427            max_concurrency,
    428            bcx.jobs(),
    429            num_cpus,
    430            self.start_str,
    431            total_time,
    432            rustc_info,
    433            error_msg,
    434        )?;
    435        Ok(())
    436    }
    437
    438    /// Write timing data in JavaScript. Primarily for `timings.js` to put data
    439    /// in a `<script>` HTML element to draw graphs.
    440    fn write_js_data(&self, f: &mut impl Write) -> CargoResult<()> {
    441        // Create a map to link indices of unlocked units.
    442        let unit_map: HashMap<Unit, usize> = self
    443            .unit_times
    444            .iter()
    445            .enumerate()
    446            .map(|(i, ut)| (ut.unit.clone(), i))
    447            .collect();
    448        #[derive(serde::Serialize)]
    449        struct UnitData {
    450            i: usize,
    451            name: String,
    452            version: String,
    453            mode: String,
    454            target: String,
    455            start: f64,
    456            duration: f64,
    457            rmeta_time: Option<f64>,
    458            unlocked_units: Vec<usize>,
    459            unlocked_rmeta_units: Vec<usize>,
    460        }
    461        let round = |x: f64| (x * 100.0).round() / 100.0;
    462        let unit_data: Vec<UnitData> = self
    463            .unit_times
    464            .iter()
    465            .enumerate()
    466            .map(|(i, ut)| {
    467                let mode = if ut.unit.mode.is_run_custom_build() {
    468                    "run-custom-build"
    469                } else {
    470                    "todo"
    471                }
    472                .to_string();
    473
    474                // These filter on the unlocked units because not all unlocked
    475                // units are actually "built". For example, Doctest mode units
    476                // don't actually generate artifacts.
    477                let unlocked_units: Vec<usize> = ut
    478                    .unlocked_units
    479                    .iter()
    480                    .filter_map(|unit| unit_map.get(unit).copied())
    481                    .collect();
    482                let unlocked_rmeta_units: Vec<usize> = ut
    483                    .unlocked_rmeta_units
    484                    .iter()
    485                    .filter_map(|unit| unit_map.get(unit).copied())
    486                    .collect();
    487                UnitData {
    488                    i,
    489                    name: ut.unit.pkg.name().to_string(),
    490                    version: ut.unit.pkg.version().to_string(),
    491                    mode,
    492                    target: ut.target.clone(),
    493                    start: round(ut.start),
    494                    duration: round(ut.duration),
    495                    rmeta_time: ut.rmeta_time.map(round),
    496                    unlocked_units,
    497                    unlocked_rmeta_units,
    498                }
    499            })
    500            .collect();
    501        writeln!(
    502            f,
    503            "const UNIT_DATA = {};",
    504            serde_json::to_string_pretty(&unit_data)?
    505        )?;
    506        writeln!(
    507            f,
    508            "const CONCURRENCY_DATA = {};",
    509            serde_json::to_string_pretty(&self.concurrency)?
    510        )?;
    511        writeln!(
    512            f,
    513            "const CPU_USAGE = {};",
    514            serde_json::to_string_pretty(&self.cpu_usage)?
    515        )?;
    516        Ok(())
    517    }
    518
    519    /// Render the table of all units.
    520    fn write_unit_table(&self, f: &mut impl Write) -> CargoResult<()> {
    521        write!(
    522            f,
    523            r#"
    524<table class="my-table">
    525  <thead>
    526    <tr>
    527      <th></th>
    528      <th>Unit</th>
    529      <th>Total</th>
    530      <th>Codegen</th>
    531      <th>Features</th>
    532    </tr>
    533  </thead>
    534  <tbody>
    535"#
    536        )?;
    537        let mut units: Vec<&UnitTime> = self.unit_times.iter().collect();
    538        units.sort_unstable_by(|a, b| b.duration.partial_cmp(&a.duration).unwrap());
    539        for (i, unit) in units.iter().enumerate() {
    540            let codegen = match unit.codegen_time() {
    541                None => "".to_string(),
    542                Some((_rt, ctime, cent)) => format!("{:.1}s ({:.0}%)", ctime, cent),
    543            };
    544            let features = unit.unit.features.join(", ");
    545            write!(
    546                f,
    547                r#"
    548<tr>
    549  <td>{}.</td>
    550  <td>{}{}</td>
    551  <td>{:.1}s</td>
    552  <td>{}</td>
    553  <td>{}</td>
    554</tr>
    555"#,
    556                i + 1,
    557                unit.name_ver(),
    558                unit.target,
    559                unit.duration,
    560                codegen,
    561                features,
    562            )?;
    563        }
    564        write!(f, "</tbody>\n</table>\n")?;
    565        Ok(())
    566    }
    567}
    568
    569impl UnitTime {
    570    /// Returns the codegen time as (`rmeta_time`, `codegen_time`, percent of total)
    571    fn codegen_time(&self) -> Option<(f64, f64, f64)> {
    572        self.rmeta_time.map(|rmeta_time| {
    573            let ctime = self.duration - rmeta_time;
    574            let cent = (ctime / self.duration) * 100.0;
    575            (rmeta_time, ctime, cent)
    576        })
    577    }
    578
    579    fn name_ver(&self) -> String {
    580        format!("{} v{}", self.unit.pkg.name(), self.unit.pkg.version())
    581    }
    582}
    583
    584fn render_rustc_info(bcx: &BuildContext<'_, '_>) -> String {
    585    let version = bcx
    586        .rustc()
    587        .verbose_version
    588        .lines()
    589        .next()
    590        .expect("rustc version");
    591    let requested_target = bcx
    592        .build_config
    593        .requested_kinds
    594        .iter()
    595        .map(|kind| bcx.target_data.short_name(kind))
    596        .collect::<Vec<_>>()
    597        .join(", ");
    598    format!(
    599        "{}<br>Host: {}<br>Target: {}",
    600        version,
    601        bcx.rustc().host,
    602        requested_target
    603    )
    604}
    605
    606static HTML_TMPL: &str = r#"
    607<html>
    608<head>
    609  <title>Cargo Build Timings — {ROOTS}</title>
    610  <meta charset="utf-8">
    611<style type="text/css">
    612:root {
    613  --error-text: #e80000;
    614  --text: #000;
    615  --background: #fff;
    616  --h1-border-bottom: #c0c0c0;
    617  --table-box-shadow: rgba(0, 0, 0, 0.1);
    618  --table-th: #d5dde5;
    619  --table-th-background: #1b1e24;
    620  --table-th-border-bottom: #9ea7af;
    621  --table-th-border-right: #343a45;
    622  --table-tr-border-top: #c1c3d1;
    623  --table-tr-border-bottom: #c1c3d1;
    624  --table-tr-odd-background: #ebebeb;
    625  --table-td-background: #ffffff;
    626  --table-td-border-right: #C1C3D1;
    627  --canvas-background: #f7f7f7;
    628  --canvas-axes: #303030;
    629  --canvas-grid: #e6e6e6;
    630  --canvas-block: #aa95e8;
    631  --canvas-custom-build: #f0b165;
    632  --canvas-not-custom-build: #95cce8;
    633  --canvas-dep-line: #ddd;
    634  --canvas-dep-line-highlighted: #000;
    635  --canvas-cpu: rgba(250, 119, 0, 0.2);
    636}
    637
    638@media (prefers-color-scheme: dark) {
    639  :root {
    640    --error-text: #e80000;
    641    --text: #fff;
    642    --background: #121212;
    643    --h1-border-bottom: #444;
    644    --table-box-shadow: rgba(255, 255, 255, 0.1);
    645    --table-th: #a0a0a0;
    646    --table-th-background: #2c2c2c;
    647    --table-th-border-bottom: #555;
    648    --table-th-border-right: #444;
    649    --table-tr-border-top: #333;
    650    --table-tr-border-bottom: #333;
    651    --table-tr-odd-background: #1e1e1e;
    652    --table-td-background: #262626;
    653    --table-td-border-right: #333;
    654    --canvas-background: #1a1a1a;
    655    --canvas-axes: #b0b0b0;
    656    --canvas-grid: #333;
    657    --canvas-block: #aa95e8;
    658    --canvas-custom-build: #f0b165;
    659    --canvas-not-custom-build: #95cce8;
    660    --canvas-dep-line: #444;
    661    --canvas-dep-line-highlighted: #fff;
    662    --canvas-cpu: rgba(250, 119, 0, 0.2);
    663  }
    664}
    665
    666html {
    667  font-family: sans-serif;
    668  color: var(--text);
    669  background: var(--background);
    670}
    671
    672.canvas-container {
    673  position: relative;
    674  margin-top: 5px;
    675  margin-bottom: 5px;
    676}
    677
    678h1 {
    679  border-bottom: 1px solid var(--h1-border-bottom);
    680}
    681
    682.graph {
    683  display: block;
    684}
    685
    686.my-table {
    687  margin-top: 20px;
    688  margin-bottom: 20px;
    689  border-collapse: collapse;
    690  box-shadow: 0 5px 10px var(--table-box-shadow);
    691}
    692
    693.my-table th {
    694  color: var(--table-th);
    695  background: var(--table-th-background);
    696  border-bottom: 4px solid var(--table-th-border-bottom);
    697  border-right: 1px solid var(--table-th-border-right);
    698  font-size: 18px;
    699  font-weight: 100;
    700  padding: 12px;
    701  text-align: left;
    702  vertical-align: middle;
    703}
    704
    705.my-table th:first-child {
    706  border-top-left-radius: 3px;
    707}
    708
    709.my-table th:last-child {
    710  border-top-right-radius: 3px;
    711  border-right:none;
    712}
    713
    714.my-table tr {
    715  border-top: 1px solid var(--table-tr-border-top);
    716  border-bottom: 1px solid var(--table-tr-border-bottom);
    717  font-size: 16px;
    718  font-weight: normal;
    719}
    720
    721.my-table tr:first-child {
    722  border-top:none;
    723}
    724
    725.my-table tr:last-child {
    726  border-bottom:none;
    727}
    728
    729.my-table tr:nth-child(odd) td {
    730  background: var(--table-tr-odd-background);
    731}
    732
    733.my-table tr:last-child td:first-child {
    734  border-bottom-left-radius:3px;
    735}
    736
    737.my-table tr:last-child td:last-child {
    738  border-bottom-right-radius:3px;
    739}
    740
    741.my-table td {
    742  background: var(--table-td-background);
    743  padding: 10px;
    744  text-align: left;
    745  vertical-align: middle;
    746  font-weight: 300;
    747  font-size: 14px;
    748  border-right: 1px solid var(--table-td-border-right);
    749}
    750
    751.my-table td:last-child {
    752  border-right: 0px;
    753}
    754
    755.summary-table td:first-child {
    756  vertical-align: top;
    757  text-align: right;
    758}
    759
    760.input-table td {
    761  text-align: center;
    762}
    763
    764.error-text {
    765  color: var(--error-text);
    766}
    767
    768</style>
    769</head>
    770<body>
    771
    772<h1>Cargo Build Timings</h1>
    773See <a href="https://doc.rust-lang.org/nightly/cargo/reference/timings.html">Documentation</a>
    774"#;
    775
    776static HTML_CANVAS: &str = r#"
    777<table class="input-table">
    778  <tr>
    779    <td><label for="min-unit-time">Min unit time:</label></td>
    780    <td><label for="scale">Scale:</label></td>
    781  </tr>
    782  <tr>
    783    <td><input type="range" min="0" max="30" step="0.1" value="0" id="min-unit-time"></td>
    784    <td><input type="range" min="1" max="50" value="20" id="scale"></td>
    785  </tr>
    786  <tr>
    787    <td><output for="min-unit-time" id="min-unit-time-output"></output></td>
    788    <td><output for="scale" id="scale-output"></output></td>
    789  </tr>
    790</table>
    791
    792<div id="pipeline-container" class="canvas-container">
    793 <canvas id="pipeline-graph" class="graph" style="position: absolute; left: 0; top: 0; z-index: 0;"></canvas>
    794 <canvas id="pipeline-graph-lines" style="position: absolute; left: 0; top: 0; z-index: 1; pointer-events:none;"></canvas>
    795</div>
    796<div class="canvas-container">
    797  <canvas id="timing-graph" class="graph"></canvas>
    798</div>
    799"#;