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