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, SystemTime};
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 = humantime::format_rfc3339_seconds(SystemTime::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"#;