1use 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 indexmap::IndexMap;
16use itertools::Itertools;
17use std::collections::HashMap;
18use std::io::{BufWriter, Write};
19use std::thread::available_parallelism;
20use std::time::{Duration, Instant};
21use tracing::warn;
22
23pub struct Timings<'gctx> {
31 gctx: &'gctx GlobalContext,
32 enabled: bool,
34 report_html: bool,
36 report_json: bool,
38 start: Instant,
40 start_str: String,
42 root_targets: Vec<(String, Vec<String>)>,
46 profile: String,
48 total_fresh: u32,
50 total_dirty: u32,
52 unit_times: Vec<UnitTime>,
54 active: HashMap<JobId, UnitTime>,
57 concurrency: Vec<Concurrency>,
60 last_cpu_state: Option<State>,
62 last_cpu_recording: Instant,
63 cpu_usage: Vec<(f64, f64)>,
67}
68
69#[derive(Copy, Clone, serde::Serialize)]
71pub struct CompilationSection {
72 start: f64,
74 end: Option<f64>,
76}
77
78struct UnitTime {
80 unit: Unit,
81 target: String,
83 start: f64,
85 duration: f64,
87 rmeta_time: Option<f64>,
90 unlocked_units: Vec<Unit>,
92 unlocked_rmeta_units: Vec<Unit>,
94 sections: IndexMap<String, CompilationSection>,
99}
100
101const FRONTEND_SECTION_NAME: &str = "Frontend";
102const CODEGEN_SECTION_NAME: &str = "Codegen";
103
104impl UnitTime {
105 fn aggregate_sections(&self) -> AggregatedSections {
106 let end = self.duration;
107
108 if !self.sections.is_empty() {
109 let mut sections = vec![];
115
116 let mut previous_section = (
119 FRONTEND_SECTION_NAME.to_string(),
120 CompilationSection {
121 start: 0.0,
122 end: None,
123 },
124 );
125 for (name, section) in self.sections.clone() {
126 sections.push((
129 previous_section.0.clone(),
130 SectionData {
131 start: previous_section.1.start,
132 end: previous_section.1.end.unwrap_or(section.start),
133 },
134 ));
135 previous_section = (name, section);
136 }
137 sections.push((
140 previous_section.0.clone(),
141 SectionData {
142 start: previous_section.1.start,
143 end: previous_section.1.end.unwrap_or(end),
144 },
145 ));
146
147 AggregatedSections::Sections(sections)
148 } else if let Some(rmeta) = self.rmeta_time {
149 AggregatedSections::OnlyMetadataTime {
151 frontend: SectionData {
152 start: 0.0,
153 end: rmeta,
154 },
155 codegen: SectionData { start: rmeta, end },
156 }
157 } else {
158 AggregatedSections::OnlyTotalDuration
160 }
161 }
162}
163
164#[derive(serde::Serialize)]
166struct Concurrency {
167 t: f64,
169 active: usize,
171 waiting: usize,
173 inactive: usize,
176}
177
178#[derive(Copy, Clone, serde::Serialize)]
180struct SectionData {
181 start: f64,
183 end: f64,
185}
186
187impl SectionData {
188 fn duration(&self) -> f64 {
189 (self.end - self.start).max(0.0)
190 }
191}
192
193#[derive(serde::Serialize)]
195enum AggregatedSections {
196 Sections(Vec<(String, SectionData)>),
198 OnlyMetadataTime {
200 frontend: SectionData,
201 codegen: SectionData,
202 },
203 OnlyTotalDuration,
205}
206
207impl<'gctx> Timings<'gctx> {
208 pub fn new(bcx: &BuildContext<'_, 'gctx>, root_units: &[Unit]) -> Timings<'gctx> {
209 let has_report = |what| bcx.build_config.timing_outputs.contains(&what);
210 let report_html = has_report(TimingOutput::Html);
211 let report_json = has_report(TimingOutput::Json);
212 let enabled = report_html | report_json;
213
214 let mut root_map: HashMap<PackageId, Vec<String>> = HashMap::new();
215 for unit in root_units {
216 let target_desc = unit.target.description_named();
217 root_map
218 .entry(unit.pkg.package_id())
219 .or_default()
220 .push(target_desc);
221 }
222 let root_targets = root_map
223 .into_iter()
224 .map(|(pkg_id, targets)| {
225 let pkg_desc = format!("{} {}", pkg_id.name(), pkg_id.version());
226 (pkg_desc, targets)
227 })
228 .collect();
229 let start_str = jiff::Timestamp::now().to_string();
230 let profile = bcx.build_config.requested_profile.to_string();
231 let last_cpu_state = if enabled {
232 match State::current() {
233 Ok(state) => Some(state),
234 Err(e) => {
235 tracing::info!("failed to get CPU state, CPU tracking disabled: {:?}", e);
236 None
237 }
238 }
239 } else {
240 None
241 };
242
243 Timings {
244 gctx: bcx.gctx,
245 enabled,
246 report_html,
247 report_json,
248 start: bcx.gctx.creation_time(),
249 start_str,
250 root_targets,
251 profile,
252 total_fresh: 0,
253 total_dirty: 0,
254 unit_times: Vec::new(),
255 active: HashMap::new(),
256 concurrency: Vec::new(),
257 last_cpu_state,
258 last_cpu_recording: Instant::now(),
259 cpu_usage: Vec::new(),
260 }
261 }
262
263 pub fn unit_start(&mut self, id: JobId, unit: Unit) {
265 if !self.enabled {
266 return;
267 }
268 let mut target = if unit.target.is_lib() && unit.mode == CompileMode::Build {
269 "".to_string()
272 } else {
273 format!(" {}", unit.target.description_named())
274 };
275 match unit.mode {
276 CompileMode::Test => target.push_str(" (test)"),
277 CompileMode::Build => {}
278 CompileMode::Check { test: true } => target.push_str(" (check-test)"),
279 CompileMode::Check { test: false } => target.push_str(" (check)"),
280 CompileMode::Doc { .. } => target.push_str(" (doc)"),
281 CompileMode::Doctest => target.push_str(" (doc test)"),
282 CompileMode::Docscrape => target.push_str(" (doc scrape)"),
283 CompileMode::RunCustomBuild => target.push_str(" (run)"),
284 }
285 let unit_time = UnitTime {
286 unit,
287 target,
288 start: self.start.elapsed().as_secs_f64(),
289 duration: 0.0,
290 rmeta_time: None,
291 unlocked_units: Vec::new(),
292 unlocked_rmeta_units: Vec::new(),
293 sections: Default::default(),
294 };
295 assert!(self.active.insert(id, unit_time).is_none());
296 }
297
298 pub fn unit_rmeta_finished(&mut self, id: JobId, unlocked: Vec<&Unit>) {
300 if !self.enabled {
301 return;
302 }
303 let Some(unit_time) = self.active.get_mut(&id) else {
307 return;
308 };
309 let t = self.start.elapsed().as_secs_f64();
310 unit_time.rmeta_time = Some(t - unit_time.start);
311 assert!(unit_time.unlocked_rmeta_units.is_empty());
312 unit_time
313 .unlocked_rmeta_units
314 .extend(unlocked.iter().cloned().cloned());
315 }
316
317 pub fn unit_finished(&mut self, id: JobId, unlocked: Vec<&Unit>) {
319 if !self.enabled {
320 return;
321 }
322 let Some(mut unit_time) = self.active.remove(&id) else {
324 return;
325 };
326 let t = self.start.elapsed().as_secs_f64();
327 unit_time.duration = t - unit_time.start;
328 assert!(unit_time.unlocked_units.is_empty());
329 unit_time
330 .unlocked_units
331 .extend(unlocked.iter().cloned().cloned());
332 if self.report_json {
333 let msg = machine_message::TimingInfo {
334 package_id: unit_time.unit.pkg.package_id().to_spec(),
335 target: &unit_time.unit.target,
336 mode: unit_time.unit.mode,
337 duration: unit_time.duration,
338 rmeta_time: unit_time.rmeta_time,
339 sections: unit_time.sections.clone().into_iter().collect(),
340 }
341 .to_json_string();
342 crate::drop_println!(self.gctx, "{}", msg);
343 }
344 self.unit_times.push(unit_time);
345 }
346
347 pub fn unit_section_timing(&mut self, id: JobId, section_timing: &SectionTiming) {
349 if !self.enabled {
350 return;
351 }
352 let Some(unit_time) = self.active.get_mut(&id) else {
353 return;
354 };
355 let now = self.start.elapsed().as_secs_f64();
356
357 match section_timing.event {
358 SectionTimingEvent::Start => {
359 unit_time.start_section(§ion_timing.name, now);
360 }
361 SectionTimingEvent::End => {
362 unit_time.end_section(§ion_timing.name, now);
363 }
364 }
365 }
366
367 pub fn mark_concurrency(&mut self, active: usize, waiting: usize, inactive: usize) {
369 if !self.enabled {
370 return;
371 }
372 let c = Concurrency {
373 t: self.start.elapsed().as_secs_f64(),
374 active,
375 waiting,
376 inactive,
377 };
378 self.concurrency.push(c);
379 }
380
381 pub fn add_fresh(&mut self) {
383 self.total_fresh += 1;
384 }
385
386 pub fn add_dirty(&mut self) {
388 self.total_dirty += 1;
389 }
390
391 pub fn record_cpu(&mut self) {
393 if !self.enabled {
394 return;
395 }
396 let Some(prev) = &mut self.last_cpu_state else {
397 return;
398 };
399 let now = Instant::now();
401 if self.last_cpu_recording.elapsed() < Duration::from_millis(100) {
402 return;
403 }
404 let current = match State::current() {
405 Ok(s) => s,
406 Err(e) => {
407 tracing::info!("failed to get CPU state: {:?}", e);
408 return;
409 }
410 };
411 let pct_idle = current.idle_since(prev);
412 *prev = current;
413 self.last_cpu_recording = now;
414 let dur = now.duration_since(self.start).as_secs_f64();
415 self.cpu_usage.push((dur, 100.0 - pct_idle));
416 }
417
418 pub fn finished(
420 &mut self,
421 build_runner: &BuildRunner<'_, '_>,
422 error: &Option<anyhow::Error>,
423 ) -> CargoResult<()> {
424 if !self.enabled {
425 return Ok(());
426 }
427 self.mark_concurrency(0, 0, 0);
428 self.unit_times
429 .sort_unstable_by(|a, b| a.start.partial_cmp(&b.start).unwrap());
430 if self.report_html {
431 self.report_html(build_runner, error)
432 .context("failed to save timing report")?;
433 }
434 Ok(())
435 }
436
437 fn report_html(
439 &self,
440 build_runner: &BuildRunner<'_, '_>,
441 error: &Option<anyhow::Error>,
442 ) -> CargoResult<()> {
443 let duration = self.start.elapsed().as_secs_f64();
444 let timestamp = self.start_str.replace(&['-', ':'][..], "");
445 let timings_path = build_runner.files().host_root().join("cargo-timings");
446 paths::create_dir_all(&timings_path)?;
447 let filename = timings_path.join(format!("cargo-timing-{}.html", timestamp));
448 let mut f = BufWriter::new(paths::create(&filename)?);
449 let roots: Vec<&str> = self
450 .root_targets
451 .iter()
452 .map(|(name, _targets)| name.as_str())
453 .collect();
454 f.write_all(HTML_TMPL.replace("{ROOTS}", &roots.join(", ")).as_bytes())?;
455 self.write_summary_table(&mut f, duration, build_runner.bcx, error)?;
456 f.write_all(HTML_CANVAS.as_bytes())?;
457 self.write_unit_table(&mut f)?;
458 writeln!(
460 f,
461 "<script>\n\
462 DURATION = {};",
463 f64::ceil(duration) as u32
464 )?;
465 self.write_js_data(&mut f)?;
466 write!(
467 f,
468 "{}\n\
469 </script>\n\
470 </body>\n\
471 </html>\n\
472 ",
473 include_str!("timings.js")
474 )?;
475 drop(f);
476
477 let unstamped_filename = timings_path.join("cargo-timing.html");
478 paths::link_or_copy(&filename, &unstamped_filename)?;
479
480 let mut shell = self.gctx.shell();
481 let timing_path = std::env::current_dir().unwrap_or_default().join(&filename);
482 let link = shell.err_file_hyperlink(&timing_path);
483 let msg = format!("report saved to {link}{}{link:#}", timing_path.display(),);
484 shell.status_with_color("Timing", msg, &style::NOTE)?;
485
486 Ok(())
487 }
488
489 fn write_summary_table(
491 &self,
492 f: &mut impl Write,
493 duration: f64,
494 bcx: &BuildContext<'_, '_>,
495 error: &Option<anyhow::Error>,
496 ) -> CargoResult<()> {
497 let targets: Vec<String> = self
498 .root_targets
499 .iter()
500 .map(|(name, targets)| format!("{} ({})", name, targets.join(", ")))
501 .collect();
502 let targets = targets.join("<br>");
503 let time_human = if duration > 60.0 {
504 format!(" ({}m {:.1}s)", duration as u32 / 60, duration % 60.0)
505 } else {
506 "".to_string()
507 };
508 let total_time = format!("{:.1}s{}", duration, time_human);
509 let max_concurrency = self.concurrency.iter().map(|c| c.active).max().unwrap();
510 let num_cpus = available_parallelism()
511 .map(|x| x.get().to_string())
512 .unwrap_or_else(|_| "n/a".into());
513 let rustc_info = render_rustc_info(bcx);
514 let error_msg = match error {
515 Some(e) => format!(r#"<tr><td class="error-text">Error:</td><td>{e}</td></tr>"#),
516 None => "".to_string(),
517 };
518 write!(
519 f,
520 r#"
521<table class="my-table summary-table">
522 <tr>
523 <td>Targets:</td><td>{}</td>
524 </tr>
525 <tr>
526 <td>Profile:</td><td>{}</td>
527 </tr>
528 <tr>
529 <td>Fresh units:</td><td>{}</td>
530 </tr>
531 <tr>
532 <td>Dirty units:</td><td>{}</td>
533 </tr>
534 <tr>
535 <td>Total units:</td><td>{}</td>
536 </tr>
537 <tr>
538 <td>Max concurrency:</td><td>{} (jobs={} ncpu={})</td>
539 </tr>
540 <tr>
541 <td>Build start:</td><td>{}</td>
542 </tr>
543 <tr>
544 <td>Total time:</td><td>{}</td>
545 </tr>
546 <tr>
547 <td>rustc:</td><td>{}</td>
548 </tr>
549{}
550</table>
551"#,
552 targets,
553 self.profile,
554 self.total_fresh,
555 self.total_dirty,
556 self.total_fresh + self.total_dirty,
557 max_concurrency,
558 bcx.jobs(),
559 num_cpus,
560 self.start_str,
561 total_time,
562 rustc_info,
563 error_msg,
564 )?;
565 Ok(())
566 }
567
568 fn write_js_data(&self, f: &mut impl Write) -> CargoResult<()> {
571 let unit_map: HashMap<Unit, usize> = self
573 .unit_times
574 .iter()
575 .enumerate()
576 .map(|(i, ut)| (ut.unit.clone(), i))
577 .collect();
578 #[derive(serde::Serialize)]
579 struct UnitData {
580 i: usize,
581 name: String,
582 version: String,
583 mode: String,
584 target: String,
585 start: f64,
586 duration: f64,
587 rmeta_time: Option<f64>,
588 unlocked_units: Vec<usize>,
589 unlocked_rmeta_units: Vec<usize>,
590 }
591 let round = |x: f64| (x * 100.0).round() / 100.0;
592 let unit_data: Vec<UnitData> = self
593 .unit_times
594 .iter()
595 .enumerate()
596 .map(|(i, ut)| {
597 let mode = if ut.unit.mode.is_run_custom_build() {
598 "run-custom-build"
599 } else {
600 "todo"
601 }
602 .to_string();
603
604 let unlocked_units: Vec<usize> = ut
608 .unlocked_units
609 .iter()
610 .filter_map(|unit| unit_map.get(unit).copied())
611 .collect();
612 let unlocked_rmeta_units: Vec<usize> = ut
613 .unlocked_rmeta_units
614 .iter()
615 .filter_map(|unit| unit_map.get(unit).copied())
616 .collect();
617 UnitData {
618 i,
619 name: ut.unit.pkg.name().to_string(),
620 version: ut.unit.pkg.version().to_string(),
621 mode,
622 target: ut.target.clone(),
623 start: round(ut.start),
624 duration: round(ut.duration),
625 rmeta_time: ut.rmeta_time.map(round),
626 unlocked_units,
627 unlocked_rmeta_units,
628 }
629 })
630 .collect();
631 writeln!(
632 f,
633 "const UNIT_DATA = {};",
634 serde_json::to_string_pretty(&unit_data)?
635 )?;
636 writeln!(
637 f,
638 "const CONCURRENCY_DATA = {};",
639 serde_json::to_string_pretty(&self.concurrency)?
640 )?;
641 writeln!(
642 f,
643 "const CPU_USAGE = {};",
644 serde_json::to_string_pretty(&self.cpu_usage)?
645 )?;
646 Ok(())
647 }
648
649 fn write_unit_table(&self, f: &mut impl Write) -> CargoResult<()> {
651 let mut units: Vec<&UnitTime> = self.unit_times.iter().collect();
652 units.sort_unstable_by(|a, b| b.duration.partial_cmp(&a.duration).unwrap());
653
654 fn capitalize(s: &str) -> String {
657 let first_char = s
658 .chars()
659 .next()
660 .map(|c| c.to_uppercase().to_string())
661 .unwrap_or_default();
662 format!("{first_char}{}", s.chars().skip(1).collect::<String>())
663 }
664
665 let aggregated: Vec<AggregatedSections> = units
671 .iter()
672 .map(|u|
673 match u.aggregate_sections() {
677 AggregatedSections::Sections(sections) => AggregatedSections::Sections(
678 sections.into_iter()
679 .map(|(name, data)| (capitalize(&name), data))
680 .collect()
681 ),
682 s => s
683 })
684 .collect();
685
686 let headers: Vec<String> = if let Some(sections) = aggregated.iter().find_map(|s| match s {
687 AggregatedSections::Sections(sections) => Some(sections),
688 _ => None,
689 }) {
690 sections.into_iter().map(|s| s.0.clone()).collect()
691 } else if aggregated
692 .iter()
693 .any(|s| matches!(s, AggregatedSections::OnlyMetadataTime { .. }))
694 {
695 vec![
696 FRONTEND_SECTION_NAME.to_string(),
697 CODEGEN_SECTION_NAME.to_string(),
698 ]
699 } else {
700 vec![]
701 };
702
703 write!(
704 f,
705 r#"
706<table class="my-table">
707 <thead>
708 <tr>
709 <th></th>
710 <th>Unit</th>
711 <th>Total</th>
712 {headers}
713 <th>Features</th>
714 </tr>
715 </thead>
716 <tbody>
717"#,
718 headers = headers.iter().map(|h| format!("<th>{h}</th>")).join("\n")
719 )?;
720
721 for (i, (unit, aggregated_sections)) in units.iter().zip(aggregated).enumerate() {
722 let format_duration = |section: Option<SectionData>| match section {
723 Some(section) => {
724 let duration = section.duration();
725 let pct = (duration / unit.duration) * 100.0;
726 format!("{duration:.1}s ({:.0}%)", pct)
727 }
728 None => "".to_string(),
729 };
730
731 let mut cells: HashMap<&str, SectionData> = Default::default();
736
737 match &aggregated_sections {
738 AggregatedSections::Sections(sections) => {
739 for (name, data) in sections {
740 cells.insert(&name, *data);
741 }
742 }
743 AggregatedSections::OnlyMetadataTime { frontend, codegen } => {
744 cells.insert(FRONTEND_SECTION_NAME, *frontend);
745 cells.insert(CODEGEN_SECTION_NAME, *codegen);
746 }
747 AggregatedSections::OnlyTotalDuration => {}
748 };
749 let cells = headers
750 .iter()
751 .map(|header| {
752 format!(
753 "<td>{}</td>",
754 format_duration(cells.remove(header.as_str()))
755 )
756 })
757 .join("\n");
758
759 let features = unit.unit.features.join(", ");
760 write!(
761 f,
762 r#"
763<tr>
764 <td>{}.</td>
765 <td>{}{}</td>
766 <td>{:.1}s</td>
767 {cells}
768 <td>{features}</td>
769</tr>
770"#,
771 i + 1,
772 unit.name_ver(),
773 unit.target,
774 unit.duration,
775 )?;
776 }
777 write!(f, "</tbody>\n</table>\n")?;
778 Ok(())
779 }
780}
781
782impl UnitTime {
783 fn name_ver(&self) -> String {
784 format!("{} v{}", self.unit.pkg.name(), self.unit.pkg.version())
785 }
786
787 fn start_section(&mut self, name: &str, now: f64) {
788 if self
789 .sections
790 .insert(
791 name.to_string(),
792 CompilationSection {
793 start: now - self.start,
794 end: None,
795 },
796 )
797 .is_some()
798 {
799 warn!("compilation section {name} started more than once");
800 }
801 }
802
803 fn end_section(&mut self, name: &str, now: f64) {
804 let Some(section) = self.sections.get_mut(name) else {
805 warn!("compilation section {name} ended, but it has no start recorded");
806 return;
807 };
808 section.end = Some(now - self.start);
809 }
810}
811
812#[derive(serde::Deserialize, Debug)]
814#[serde(rename_all = "kebab-case")]
815pub enum SectionTimingEvent {
816 Start,
817 End,
818}
819
820#[derive(serde::Deserialize, Debug)]
823pub struct SectionTiming {
824 pub name: String,
825 pub event: SectionTimingEvent,
826}
827
828fn render_rustc_info(bcx: &BuildContext<'_, '_>) -> String {
829 let version = bcx
830 .rustc()
831 .verbose_version
832 .lines()
833 .next()
834 .expect("rustc version");
835 let requested_target = bcx
836 .build_config
837 .requested_kinds
838 .iter()
839 .map(|kind| bcx.target_data.short_name(kind))
840 .collect::<Vec<_>>()
841 .join(", ");
842 format!(
843 "{}<br>Host: {}<br>Target: {}",
844 version,
845 bcx.rustc().host,
846 requested_target
847 )
848}
849
850static HTML_TMPL: &str = r#"
851<html>
852<head>
853 <title>Cargo Build Timings — {ROOTS}</title>
854 <meta charset="utf-8">
855<style type="text/css">
856:root {
857 --error-text: #e80000;
858 --text: #000;
859 --background: #fff;
860 --h1-border-bottom: #c0c0c0;
861 --table-box-shadow: rgba(0, 0, 0, 0.1);
862 --table-th: #d5dde5;
863 --table-th-background: #1b1e24;
864 --table-th-border-bottom: #9ea7af;
865 --table-th-border-right: #343a45;
866 --table-tr-border-top: #c1c3d1;
867 --table-tr-border-bottom: #c1c3d1;
868 --table-tr-odd-background: #ebebeb;
869 --table-td-background: #ffffff;
870 --table-td-border-right: #C1C3D1;
871 --canvas-background: #f7f7f7;
872 --canvas-axes: #303030;
873 --canvas-grid: #e6e6e6;
874 --canvas-block: #aa95e8;
875 --canvas-custom-build: #f0b165;
876 --canvas-not-custom-build: #95cce8;
877 --canvas-dep-line: #ddd;
878 --canvas-dep-line-highlighted: #000;
879 --canvas-cpu: rgba(250, 119, 0, 0.2);
880}
881
882@media (prefers-color-scheme: dark) {
883 :root {
884 --error-text: #e80000;
885 --text: #fff;
886 --background: #121212;
887 --h1-border-bottom: #444;
888 --table-box-shadow: rgba(255, 255, 255, 0.1);
889 --table-th: #a0a0a0;
890 --table-th-background: #2c2c2c;
891 --table-th-border-bottom: #555;
892 --table-th-border-right: #444;
893 --table-tr-border-top: #333;
894 --table-tr-border-bottom: #333;
895 --table-tr-odd-background: #1e1e1e;
896 --table-td-background: #262626;
897 --table-td-border-right: #333;
898 --canvas-background: #1a1a1a;
899 --canvas-axes: #b0b0b0;
900 --canvas-grid: #333;
901 --canvas-block: #aa95e8;
902 --canvas-custom-build: #f0b165;
903 --canvas-not-custom-build: #95cce8;
904 --canvas-dep-line: #444;
905 --canvas-dep-line-highlighted: #fff;
906 --canvas-cpu: rgba(250, 119, 0, 0.2);
907 }
908}
909
910html {
911 font-family: sans-serif;
912 color: var(--text);
913 background: var(--background);
914}
915
916.canvas-container {
917 position: relative;
918 margin-top: 5px;
919 margin-bottom: 5px;
920}
921
922h1 {
923 border-bottom: 1px solid var(--h1-border-bottom);
924}
925
926.graph {
927 display: block;
928}
929
930.my-table {
931 margin-top: 20px;
932 margin-bottom: 20px;
933 border-collapse: collapse;
934 box-shadow: 0 5px 10px var(--table-box-shadow);
935}
936
937.my-table th {
938 color: var(--table-th);
939 background: var(--table-th-background);
940 border-bottom: 4px solid var(--table-th-border-bottom);
941 border-right: 1px solid var(--table-th-border-right);
942 font-size: 18px;
943 font-weight: 100;
944 padding: 12px;
945 text-align: left;
946 vertical-align: middle;
947}
948
949.my-table th:first-child {
950 border-top-left-radius: 3px;
951}
952
953.my-table th:last-child {
954 border-top-right-radius: 3px;
955 border-right:none;
956}
957
958.my-table tr {
959 border-top: 1px solid var(--table-tr-border-top);
960 border-bottom: 1px solid var(--table-tr-border-bottom);
961 font-size: 16px;
962 font-weight: normal;
963}
964
965.my-table tr:first-child {
966 border-top:none;
967}
968
969.my-table tr:last-child {
970 border-bottom:none;
971}
972
973.my-table tr:nth-child(odd) td {
974 background: var(--table-tr-odd-background);
975}
976
977.my-table tr:last-child td:first-child {
978 border-bottom-left-radius:3px;
979}
980
981.my-table tr:last-child td:last-child {
982 border-bottom-right-radius:3px;
983}
984
985.my-table td {
986 background: var(--table-td-background);
987 padding: 10px;
988 text-align: left;
989 vertical-align: middle;
990 font-weight: 300;
991 font-size: 14px;
992 border-right: 1px solid var(--table-td-border-right);
993}
994
995.my-table td:last-child {
996 border-right: 0px;
997}
998
999.summary-table td:first-child {
1000 vertical-align: top;
1001 text-align: right;
1002}
1003
1004.input-table td {
1005 text-align: center;
1006}
1007
1008.error-text {
1009 color: var(--error-text);
1010}
1011
1012</style>
1013</head>
1014<body>
1015
1016<h1>Cargo Build Timings</h1>
1017See <a href="https://doc.rust-lang.org/nightly/cargo/reference/timings.html">Documentation</a>
1018"#;
1019
1020static HTML_CANVAS: &str = r#"
1021<table class="input-table">
1022 <tr>
1023 <td><label for="min-unit-time">Min unit time:</label></td>
1024 <td title="Scale corresponds to a number of pixels per second. It is automatically initialized based on your viewport width.">
1025 <label for="scale">Scale:</label>
1026 </td>
1027 </tr>
1028 <tr>
1029 <td><input type="range" min="0" max="30" step="0.1" value="0" id="min-unit-time"></td>
1030 <!--
1031 The scale corresponds to some number of "pixels per second".
1032 Its min, max, and initial values are automatically set by JavaScript on page load,
1033 based on the client viewport.
1034 -->
1035 <td><input type="range" min="1" max="100" value="50" id="scale"></td>
1036 </tr>
1037 <tr>
1038 <td><output for="min-unit-time" id="min-unit-time-output"></output></td>
1039 <td><output for="scale" id="scale-output"></output></td>
1040 </tr>
1041</table>
1042
1043<div id="pipeline-container" class="canvas-container">
1044 <canvas id="pipeline-graph" class="graph" style="position: absolute; left: 0; top: 0; z-index: 0;"></canvas>
1045 <canvas id="pipeline-graph-lines" style="position: absolute; left: 0; top: 0; z-index: 1; pointer-events:none;"></canvas>
1046</div>
1047<div class="canvas-container">
1048 <canvas id="timing-graph" class="graph"></canvas>
1049</div>
1050"#;