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::PackageId;
7use crate::core::compiler::job_queue::JobId;
8use crate::core::compiler::{BuildContext, BuildRunner, TimingOutput};
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::Doc { .. } => target.push_str(" (doc)"),
172            CompileMode::Doctest => target.push_str(" (doc test)"),
173            CompileMode::Docscrape => target.push_str(" (doc scrape)"),
174            CompileMode::RunCustomBuild => target.push_str(" (run)"),
175        }
176        let unit_time = UnitTime {
177            unit,
178            target,
179            start: self.start.elapsed().as_secs_f64(),
180            duration: 0.0,
181            rmeta_time: None,
182            unlocked_units: Vec::new(),
183            unlocked_rmeta_units: Vec::new(),
184        };
185        assert!(self.active.insert(id, unit_time).is_none());
186    }
187
188    /// Mark that the `.rmeta` file as generated.
189    pub fn unit_rmeta_finished(&mut self, id: JobId, unlocked: Vec<&Unit>) {
190        if !self.enabled {
191            return;
192        }
193        // `id` may not always be active. "fresh" units unconditionally
194        // generate `Message::Finish`, but this active map only tracks dirty
195        // units.
196        let Some(unit_time) = self.active.get_mut(&id) else {
197            return;
198        };
199        let t = self.start.elapsed().as_secs_f64();
200        unit_time.rmeta_time = Some(t - unit_time.start);
201        assert!(unit_time.unlocked_rmeta_units.is_empty());
202        unit_time
203            .unlocked_rmeta_units
204            .extend(unlocked.iter().cloned().cloned());
205    }
206
207    /// Mark that a unit has finished running.
208    pub fn unit_finished(&mut self, id: JobId, unlocked: Vec<&Unit>) {
209        if !self.enabled {
210            return;
211        }
212        // See note above in `unit_rmeta_finished`, this may not always be active.
213        let Some(mut unit_time) = self.active.remove(&id) else {
214            return;
215        };
216        let t = self.start.elapsed().as_secs_f64();
217        unit_time.duration = t - unit_time.start;
218        assert!(unit_time.unlocked_units.is_empty());
219        unit_time
220            .unlocked_units
221            .extend(unlocked.iter().cloned().cloned());
222        if self.report_json {
223            let msg = machine_message::TimingInfo {
224                package_id: unit_time.unit.pkg.package_id().to_spec(),
225                target: &unit_time.unit.target,
226                mode: unit_time.unit.mode,
227                duration: unit_time.duration,
228                rmeta_time: unit_time.rmeta_time,
229            }
230            .to_json_string();
231            crate::drop_println!(self.gctx, "{}", msg);
232        }
233        self.unit_times.push(unit_time);
234    }
235
236    /// This is called periodically to mark the concurrency of internal structures.
237    pub fn mark_concurrency(&mut self, active: usize, waiting: usize, inactive: usize) {
238        if !self.enabled {
239            return;
240        }
241        let c = Concurrency {
242            t: self.start.elapsed().as_secs_f64(),
243            active,
244            waiting,
245            inactive,
246        };
247        self.concurrency.push(c);
248    }
249
250    /// Mark that a fresh unit was encountered. (No re-compile needed)
251    pub fn add_fresh(&mut self) {
252        self.total_fresh += 1;
253    }
254
255    /// Mark that a dirty unit was encountered. (Re-compile needed)
256    pub fn add_dirty(&mut self) {
257        self.total_dirty += 1;
258    }
259
260    /// Take a sample of CPU usage
261    pub fn record_cpu(&mut self) {
262        if !self.enabled {
263            return;
264        }
265        let Some(prev) = &mut self.last_cpu_state else {
266            return;
267        };
268        // Don't take samples too frequently, even if requested.
269        let now = Instant::now();
270        if self.last_cpu_recording.elapsed() < Duration::from_millis(100) {
271            return;
272        }
273        let current = match State::current() {
274            Ok(s) => s,
275            Err(e) => {
276                tracing::info!("failed to get CPU state: {:?}", e);
277                return;
278            }
279        };
280        let pct_idle = current.idle_since(prev);
281        *prev = current;
282        self.last_cpu_recording = now;
283        let dur = now.duration_since(self.start).as_secs_f64();
284        self.cpu_usage.push((dur, 100.0 - pct_idle));
285    }
286
287    /// Call this when all units are finished.
288    pub fn finished(
289        &mut self,
290        build_runner: &BuildRunner<'_, '_>,
291        error: &Option<anyhow::Error>,
292    ) -> CargoResult<()> {
293        if !self.enabled {
294            return Ok(());
295        }
296        self.mark_concurrency(0, 0, 0);
297        self.unit_times
298            .sort_unstable_by(|a, b| a.start.partial_cmp(&b.start).unwrap());
299        if self.report_html {
300            self.report_html(build_runner, error)
301                .context("failed to save timing report")?;
302        }
303        Ok(())
304    }
305
306    /// Save HTML report to disk.
307    fn report_html(
308        &self,
309        build_runner: &BuildRunner<'_, '_>,
310        error: &Option<anyhow::Error>,
311    ) -> CargoResult<()> {
312        let duration = self.start.elapsed().as_secs_f64();
313        let timestamp = self.start_str.replace(&['-', ':'][..], "");
314        let timings_path = build_runner.files().host_root().join("cargo-timings");
315        paths::create_dir_all(&timings_path)?;
316        let filename = timings_path.join(format!("cargo-timing-{}.html", timestamp));
317        let mut f = BufWriter::new(paths::create(&filename)?);
318        let roots: Vec<&str> = self
319            .root_targets
320            .iter()
321            .map(|(name, _targets)| name.as_str())
322            .collect();
323        f.write_all(HTML_TMPL.replace("{ROOTS}", &roots.join(", ")).as_bytes())?;
324        self.write_summary_table(&mut f, duration, build_runner.bcx, error)?;
325        f.write_all(HTML_CANVAS.as_bytes())?;
326        self.write_unit_table(&mut f)?;
327        // It helps with pixel alignment to use whole numbers.
328        writeln!(
329            f,
330            "<script>\n\
331             DURATION = {};",
332            f64::ceil(duration) as u32
333        )?;
334        self.write_js_data(&mut f)?;
335        write!(
336            f,
337            "{}\n\
338             </script>\n\
339             </body>\n\
340             </html>\n\
341             ",
342            include_str!("timings.js")
343        )?;
344        drop(f);
345
346        let unstamped_filename = timings_path.join("cargo-timing.html");
347        paths::link_or_copy(&filename, &unstamped_filename)?;
348
349        let mut shell = self.gctx.shell();
350        let timing_path = std::env::current_dir().unwrap_or_default().join(&filename);
351        let link = shell.err_file_hyperlink(&timing_path);
352        let msg = format!("report saved to {link}{}{link:#}", timing_path.display(),);
353        shell.status_with_color("Timing", msg, &style::NOTE)?;
354
355        Ok(())
356    }
357
358    /// Render the summary table.
359    fn write_summary_table(
360        &self,
361        f: &mut impl Write,
362        duration: f64,
363        bcx: &BuildContext<'_, '_>,
364        error: &Option<anyhow::Error>,
365    ) -> CargoResult<()> {
366        let targets: Vec<String> = self
367            .root_targets
368            .iter()
369            .map(|(name, targets)| format!("{} ({})", name, targets.join(", ")))
370            .collect();
371        let targets = targets.join("<br>");
372        let time_human = if duration > 60.0 {
373            format!(" ({}m {:.1}s)", duration as u32 / 60, duration % 60.0)
374        } else {
375            "".to_string()
376        };
377        let total_time = format!("{:.1}s{}", duration, time_human);
378        let max_concurrency = self.concurrency.iter().map(|c| c.active).max().unwrap();
379        let num_cpus = available_parallelism()
380            .map(|x| x.get().to_string())
381            .unwrap_or_else(|_| "n/a".into());
382        let rustc_info = render_rustc_info(bcx);
383        let error_msg = match error {
384            Some(e) => format!(r#"<tr><td class="error-text">Error:</td><td>{e}</td></tr>"#),
385            None => "".to_string(),
386        };
387        write!(
388            f,
389            r#"
390<table class="my-table summary-table">
391  <tr>
392    <td>Targets:</td><td>{}</td>
393  </tr>
394  <tr>
395    <td>Profile:</td><td>{}</td>
396  </tr>
397  <tr>
398    <td>Fresh units:</td><td>{}</td>
399  </tr>
400  <tr>
401    <td>Dirty units:</td><td>{}</td>
402  </tr>
403  <tr>
404    <td>Total units:</td><td>{}</td>
405  </tr>
406  <tr>
407    <td>Max concurrency:</td><td>{} (jobs={} ncpu={})</td>
408  </tr>
409  <tr>
410    <td>Build start:</td><td>{}</td>
411  </tr>
412  <tr>
413    <td>Total time:</td><td>{}</td>
414  </tr>
415  <tr>
416    <td>rustc:</td><td>{}</td>
417  </tr>
418{}
419</table>
420"#,
421            targets,
422            self.profile,
423            self.total_fresh,
424            self.total_dirty,
425            self.total_fresh + self.total_dirty,
426            max_concurrency,
427            bcx.jobs(),
428            num_cpus,
429            self.start_str,
430            total_time,
431            rustc_info,
432            error_msg,
433        )?;
434        Ok(())
435    }
436
437    /// Write timing data in JavaScript. Primarily for `timings.js` to put data
438    /// in a `<script>` HTML element to draw graphs.
439    fn write_js_data(&self, f: &mut impl Write) -> CargoResult<()> {
440        // Create a map to link indices of unlocked units.
441        let unit_map: HashMap<Unit, usize> = self
442            .unit_times
443            .iter()
444            .enumerate()
445            .map(|(i, ut)| (ut.unit.clone(), i))
446            .collect();
447        #[derive(serde::Serialize)]
448        struct UnitData {
449            i: usize,
450            name: String,
451            version: String,
452            mode: String,
453            target: String,
454            start: f64,
455            duration: f64,
456            rmeta_time: Option<f64>,
457            unlocked_units: Vec<usize>,
458            unlocked_rmeta_units: Vec<usize>,
459        }
460        let round = |x: f64| (x * 100.0).round() / 100.0;
461        let unit_data: Vec<UnitData> = self
462            .unit_times
463            .iter()
464            .enumerate()
465            .map(|(i, ut)| {
466                let mode = if ut.unit.mode.is_run_custom_build() {
467                    "run-custom-build"
468                } else {
469                    "todo"
470                }
471                .to_string();
472
473                // These filter on the unlocked units because not all unlocked
474                // units are actually "built". For example, Doctest mode units
475                // don't actually generate artifacts.
476                let unlocked_units: Vec<usize> = ut
477                    .unlocked_units
478                    .iter()
479                    .filter_map(|unit| unit_map.get(unit).copied())
480                    .collect();
481                let unlocked_rmeta_units: Vec<usize> = ut
482                    .unlocked_rmeta_units
483                    .iter()
484                    .filter_map(|unit| unit_map.get(unit).copied())
485                    .collect();
486                UnitData {
487                    i,
488                    name: ut.unit.pkg.name().to_string(),
489                    version: ut.unit.pkg.version().to_string(),
490                    mode,
491                    target: ut.target.clone(),
492                    start: round(ut.start),
493                    duration: round(ut.duration),
494                    rmeta_time: ut.rmeta_time.map(round),
495                    unlocked_units,
496                    unlocked_rmeta_units,
497                }
498            })
499            .collect();
500        writeln!(
501            f,
502            "const UNIT_DATA = {};",
503            serde_json::to_string_pretty(&unit_data)?
504        )?;
505        writeln!(
506            f,
507            "const CONCURRENCY_DATA = {};",
508            serde_json::to_string_pretty(&self.concurrency)?
509        )?;
510        writeln!(
511            f,
512            "const CPU_USAGE = {};",
513            serde_json::to_string_pretty(&self.cpu_usage)?
514        )?;
515        Ok(())
516    }
517
518    /// Render the table of all units.
519    fn write_unit_table(&self, f: &mut impl Write) -> CargoResult<()> {
520        write!(
521            f,
522            r#"
523<table class="my-table">
524  <thead>
525    <tr>
526      <th></th>
527      <th>Unit</th>
528      <th>Total</th>
529      <th>Codegen</th>
530      <th>Features</th>
531    </tr>
532  </thead>
533  <tbody>
534"#
535        )?;
536        let mut units: Vec<&UnitTime> = self.unit_times.iter().collect();
537        units.sort_unstable_by(|a, b| b.duration.partial_cmp(&a.duration).unwrap());
538        for (i, unit) in units.iter().enumerate() {
539            let codegen = match unit.codegen_time() {
540                None => "".to_string(),
541                Some((_rt, ctime, cent)) => format!("{:.1}s ({:.0}%)", ctime, cent),
542            };
543            let features = unit.unit.features.join(", ");
544            write!(
545                f,
546                r#"
547<tr>
548  <td>{}.</td>
549  <td>{}{}</td>
550  <td>{:.1}s</td>
551  <td>{}</td>
552  <td>{}</td>
553</tr>
554"#,
555                i + 1,
556                unit.name_ver(),
557                unit.target,
558                unit.duration,
559                codegen,
560                features,
561            )?;
562        }
563        write!(f, "</tbody>\n</table>\n")?;
564        Ok(())
565    }
566}
567
568impl UnitTime {
569    /// Returns the codegen time as (`rmeta_time`, `codegen_time`, percent of total)
570    fn codegen_time(&self) -> Option<(f64, f64, f64)> {
571        self.rmeta_time.map(|rmeta_time| {
572            let ctime = self.duration - rmeta_time;
573            let cent = (ctime / self.duration) * 100.0;
574            (rmeta_time, ctime, cent)
575        })
576    }
577
578    fn name_ver(&self) -> String {
579        format!("{} v{}", self.unit.pkg.name(), self.unit.pkg.version())
580    }
581}
582
583fn render_rustc_info(bcx: &BuildContext<'_, '_>) -> String {
584    let version = bcx
585        .rustc()
586        .verbose_version
587        .lines()
588        .next()
589        .expect("rustc version");
590    let requested_target = bcx
591        .build_config
592        .requested_kinds
593        .iter()
594        .map(|kind| bcx.target_data.short_name(kind))
595        .collect::<Vec<_>>()
596        .join(", ");
597    format!(
598        "{}<br>Host: {}<br>Target: {}",
599        version,
600        bcx.rustc().host,
601        requested_target
602    )
603}
604
605static HTML_TMPL: &str = r#"
606<html>
607<head>
608  <title>Cargo Build Timings — {ROOTS}</title>
609  <meta charset="utf-8">
610<style type="text/css">
611:root {
612  --error-text: #e80000;
613  --text: #000;
614  --background: #fff;
615  --h1-border-bottom: #c0c0c0;
616  --table-box-shadow: rgba(0, 0, 0, 0.1);
617  --table-th: #d5dde5;
618  --table-th-background: #1b1e24;
619  --table-th-border-bottom: #9ea7af;
620  --table-th-border-right: #343a45;
621  --table-tr-border-top: #c1c3d1;
622  --table-tr-border-bottom: #c1c3d1;
623  --table-tr-odd-background: #ebebeb;
624  --table-td-background: #ffffff;
625  --table-td-border-right: #C1C3D1;
626  --canvas-background: #f7f7f7;
627  --canvas-axes: #303030;
628  --canvas-grid: #e6e6e6;
629  --canvas-block: #aa95e8;
630  --canvas-custom-build: #f0b165;
631  --canvas-not-custom-build: #95cce8;
632  --canvas-dep-line: #ddd;
633  --canvas-dep-line-highlighted: #000;
634  --canvas-cpu: rgba(250, 119, 0, 0.2);
635}
636
637@media (prefers-color-scheme: dark) {
638  :root {
639    --error-text: #e80000;
640    --text: #fff;
641    --background: #121212;
642    --h1-border-bottom: #444;
643    --table-box-shadow: rgba(255, 255, 255, 0.1);
644    --table-th: #a0a0a0;
645    --table-th-background: #2c2c2c;
646    --table-th-border-bottom: #555;
647    --table-th-border-right: #444;
648    --table-tr-border-top: #333;
649    --table-tr-border-bottom: #333;
650    --table-tr-odd-background: #1e1e1e;
651    --table-td-background: #262626;
652    --table-td-border-right: #333;
653    --canvas-background: #1a1a1a;
654    --canvas-axes: #b0b0b0;
655    --canvas-grid: #333;
656    --canvas-block: #aa95e8;
657    --canvas-custom-build: #f0b165;
658    --canvas-not-custom-build: #95cce8;
659    --canvas-dep-line: #444;
660    --canvas-dep-line-highlighted: #fff;
661    --canvas-cpu: rgba(250, 119, 0, 0.2);
662  }
663}
664
665html {
666  font-family: sans-serif;
667  color: var(--text);
668  background: var(--background);
669}
670
671.canvas-container {
672  position: relative;
673  margin-top: 5px;
674  margin-bottom: 5px;
675}
676
677h1 {
678  border-bottom: 1px solid var(--h1-border-bottom);
679}
680
681.graph {
682  display: block;
683}
684
685.my-table {
686  margin-top: 20px;
687  margin-bottom: 20px;
688  border-collapse: collapse;
689  box-shadow: 0 5px 10px var(--table-box-shadow);
690}
691
692.my-table th {
693  color: var(--table-th);
694  background: var(--table-th-background);
695  border-bottom: 4px solid var(--table-th-border-bottom);
696  border-right: 1px solid var(--table-th-border-right);
697  font-size: 18px;
698  font-weight: 100;
699  padding: 12px;
700  text-align: left;
701  vertical-align: middle;
702}
703
704.my-table th:first-child {
705  border-top-left-radius: 3px;
706}
707
708.my-table th:last-child {
709  border-top-right-radius: 3px;
710  border-right:none;
711}
712
713.my-table tr {
714  border-top: 1px solid var(--table-tr-border-top);
715  border-bottom: 1px solid var(--table-tr-border-bottom);
716  font-size: 16px;
717  font-weight: normal;
718}
719
720.my-table tr:first-child {
721  border-top:none;
722}
723
724.my-table tr:last-child {
725  border-bottom:none;
726}
727
728.my-table tr:nth-child(odd) td {
729  background: var(--table-tr-odd-background);
730}
731
732.my-table tr:last-child td:first-child {
733  border-bottom-left-radius:3px;
734}
735
736.my-table tr:last-child td:last-child {
737  border-bottom-right-radius:3px;
738}
739
740.my-table td {
741  background: var(--table-td-background);
742  padding: 10px;
743  text-align: left;
744  vertical-align: middle;
745  font-weight: 300;
746  font-size: 14px;
747  border-right: 1px solid var(--table-td-border-right);
748}
749
750.my-table td:last-child {
751  border-right: 0px;
752}
753
754.summary-table td:first-child {
755  vertical-align: top;
756  text-align: right;
757}
758
759.input-table td {
760  text-align: center;
761}
762
763.error-text {
764  color: var(--error-text);
765}
766
767</style>
768</head>
769<body>
770
771<h1>Cargo Build Timings</h1>
772See <a href="https://doc.rust-lang.org/nightly/cargo/reference/timings.html">Documentation</a>
773"#;
774
775static HTML_CANVAS: &str = r#"
776<table class="input-table">
777  <tr>
778    <td><label for="min-unit-time">Min unit time:</label></td>
779    <td title="Scale corresponds to a number of pixels per second. It is automatically initialized based on your viewport width.">
780      <label for="scale">Scale:</label>
781    </td>
782  </tr>
783  <tr>
784    <td><input type="range" min="0" max="30" step="0.1" value="0" id="min-unit-time"></td>
785    <!--
786        The scale corresponds to some number of "pixels per second".
787        Its min, max, and initial values are automatically set by JavaScript on page load,
788        based on the client viewport.
789    -->
790    <td><input type="range" min="1" max="100" value="50" id="scale"></td>
791  </tr>
792  <tr>
793    <td><output for="min-unit-time" id="min-unit-time-output"></output></td>
794    <td><output for="scale" id="scale-output"></output></td>
795  </tr>
796</table>
797
798<div id="pipeline-container" class="canvas-container">
799 <canvas id="pipeline-graph" class="graph" style="position: absolute; left: 0; top: 0; z-index: 0;"></canvas>
800 <canvas id="pipeline-graph-lines" style="position: absolute; left: 0; top: 0; z-index: 1; pointer-events:none;"></canvas>
801</div>
802<div class="canvas-container">
803  <canvas id="timing-graph" class="graph"></canvas>
804</div>
805"#;