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::log_message::LogMessage;
11use crate::util::machine_message::{self, Message};
12use crate::util::style;
13use crate::util::{CargoResult, GlobalContext};
14use anyhow::Context as _;
15use cargo_util::paths;
16use indexmap::IndexMap;
17use itertools::Itertools;
18use std::collections::HashMap;
19use std::io::{BufWriter, Write};
20use std::thread::available_parallelism;
21use std::time::{Duration, Instant};
22use tracing::warn;
23
24pub struct Timings<'gctx> {
32 gctx: &'gctx GlobalContext,
33 enabled: bool,
35 report_html: bool,
37 report_json: bool,
39 start: Instant,
41 start_str: String,
43 root_targets: Vec<(String, Vec<String>)>,
47 profile: String,
49 total_fresh: u32,
51 total_dirty: u32,
53 unit_times: Vec<UnitTime>,
55 active: HashMap<JobId, UnitTime>,
58 concurrency: Vec<Concurrency>,
61 last_cpu_state: Option<State>,
63 last_cpu_recording: Instant,
64 cpu_usage: Vec<(f64, f64)>,
68}
69
70#[derive(Copy, Clone, serde::Serialize)]
72pub struct CompilationSection {
73 start: f64,
75 end: Option<f64>,
77}
78
79struct UnitTime {
81 unit: Unit,
82 target: String,
84 start: f64,
86 duration: f64,
88 rmeta_time: Option<f64>,
91 unlocked_units: Vec<Unit>,
93 unlocked_rmeta_units: Vec<Unit>,
95 sections: IndexMap<String, CompilationSection>,
100}
101
102const FRONTEND_SECTION_NAME: &str = "Frontend";
103const CODEGEN_SECTION_NAME: &str = "Codegen";
104
105impl UnitTime {
106 fn aggregate_sections(&self) -> AggregatedSections {
107 let end = self.duration;
108
109 if !self.sections.is_empty() {
110 let mut sections = vec![];
116
117 let mut previous_section = (
120 FRONTEND_SECTION_NAME.to_string(),
121 CompilationSection {
122 start: 0.0,
123 end: None,
124 },
125 );
126 for (name, section) in self.sections.clone() {
127 sections.push((
130 previous_section.0.clone(),
131 SectionData {
132 start: previous_section.1.start,
133 end: previous_section.1.end.unwrap_or(section.start),
134 },
135 ));
136 previous_section = (name, section);
137 }
138 sections.push((
141 previous_section.0.clone(),
142 SectionData {
143 start: previous_section.1.start,
144 end: previous_section.1.end.unwrap_or(end),
145 },
146 ));
147
148 AggregatedSections::Sections(sections)
149 } else if let Some(rmeta) = self.rmeta_time {
150 AggregatedSections::OnlyMetadataTime {
152 frontend: SectionData {
153 start: 0.0,
154 end: rmeta,
155 },
156 codegen: SectionData { start: rmeta, end },
157 }
158 } else {
159 AggregatedSections::OnlyTotalDuration
161 }
162 }
163}
164
165#[derive(serde::Serialize)]
167struct Concurrency {
168 t: f64,
170 active: usize,
172 waiting: usize,
174 inactive: usize,
177}
178
179#[derive(Copy, Clone, serde::Serialize)]
181struct SectionData {
182 start: f64,
184 end: f64,
186}
187
188impl SectionData {
189 fn duration(&self) -> f64 {
190 (self.end - self.start).max(0.0)
191 }
192}
193
194enum 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 | bcx.logger.is_some();
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(
319 &mut self,
320 build_runner: &BuildRunner<'_, '_>,
321 id: JobId,
322 unlocked: Vec<&Unit>,
323 ) {
324 if !self.enabled {
325 return;
326 }
327 let Some(mut unit_time) = self.active.remove(&id) else {
329 return;
330 };
331 let t = self.start.elapsed().as_secs_f64();
332 unit_time.duration = t - unit_time.start;
333 assert!(unit_time.unlocked_units.is_empty());
334 unit_time
335 .unlocked_units
336 .extend(unlocked.iter().cloned().cloned());
337 if self.report_json {
338 let msg = machine_message::TimingInfo {
339 package_id: unit_time.unit.pkg.package_id().to_spec(),
340 target: &unit_time.unit.target,
341 mode: unit_time.unit.mode,
342 duration: unit_time.duration,
343 rmeta_time: unit_time.rmeta_time,
344 sections: unit_time.sections.clone().into_iter().collect(),
345 }
346 .to_json_string();
347 crate::drop_println!(self.gctx, "{}", msg);
348 }
349 if let Some(logger) = build_runner.bcx.logger {
350 logger.log(LogMessage::TimingInfo {
351 package_id: unit_time.unit.pkg.package_id().to_spec(),
352 target: unit_time.unit.target.clone(),
353 mode: unit_time.unit.mode,
354 duration: unit_time.duration,
355 rmeta_time: unit_time.rmeta_time,
356 sections: unit_time.sections.clone().into_iter().collect(),
357 });
358 }
359 self.unit_times.push(unit_time);
360 }
361
362 pub fn unit_section_timing(&mut self, id: JobId, section_timing: &SectionTiming) {
364 if !self.enabled {
365 return;
366 }
367 let Some(unit_time) = self.active.get_mut(&id) else {
368 return;
369 };
370 let now = self.start.elapsed().as_secs_f64();
371
372 match section_timing.event {
373 SectionTimingEvent::Start => {
374 unit_time.start_section(§ion_timing.name, now);
375 }
376 SectionTimingEvent::End => {
377 unit_time.end_section(§ion_timing.name, now);
378 }
379 }
380 }
381
382 pub fn mark_concurrency(&mut self, active: usize, waiting: usize, inactive: usize) {
384 if !self.enabled {
385 return;
386 }
387 let c = Concurrency {
388 t: self.start.elapsed().as_secs_f64(),
389 active,
390 waiting,
391 inactive,
392 };
393 self.concurrency.push(c);
394 }
395
396 pub fn add_fresh(&mut self) {
398 self.total_fresh += 1;
399 }
400
401 pub fn add_dirty(&mut self) {
403 self.total_dirty += 1;
404 }
405
406 pub fn record_cpu(&mut self) {
408 if !self.enabled {
409 return;
410 }
411 let Some(prev) = &mut self.last_cpu_state else {
412 return;
413 };
414 let now = Instant::now();
416 if self.last_cpu_recording.elapsed() < Duration::from_millis(100) {
417 return;
418 }
419 let current = match State::current() {
420 Ok(s) => s,
421 Err(e) => {
422 tracing::info!("failed to get CPU state: {:?}", e);
423 return;
424 }
425 };
426 let pct_idle = current.idle_since(prev);
427 *prev = current;
428 self.last_cpu_recording = now;
429 let dur = now.duration_since(self.start).as_secs_f64();
430 self.cpu_usage.push((dur, 100.0 - pct_idle));
431 }
432
433 pub fn finished(
435 &mut self,
436 build_runner: &BuildRunner<'_, '_>,
437 error: &Option<anyhow::Error>,
438 ) -> CargoResult<()> {
439 if !self.enabled {
440 return Ok(());
441 }
442 self.mark_concurrency(0, 0, 0);
443 self.unit_times
444 .sort_unstable_by(|a, b| a.start.partial_cmp(&b.start).unwrap());
445 if self.report_html {
446 self.report_html(build_runner, error)
447 .context("failed to save timing report")?;
448 }
449 Ok(())
450 }
451
452 fn report_html(
454 &self,
455 build_runner: &BuildRunner<'_, '_>,
456 error: &Option<anyhow::Error>,
457 ) -> CargoResult<()> {
458 let duration = self.start.elapsed().as_secs_f64();
459 let timestamp = self.start_str.replace(&['-', ':'][..], "");
460 let timings_path = build_runner.files().timings_dir();
461 paths::create_dir_all(&timings_path)?;
462 let filename = timings_path.join(format!("cargo-timing-{}.html", timestamp));
463 let mut f = BufWriter::new(paths::create(&filename)?);
464 let roots: Vec<&str> = self
465 .root_targets
466 .iter()
467 .map(|(name, _targets)| name.as_str())
468 .collect();
469 f.write_all(HTML_TMPL.replace("{ROOTS}", &roots.join(", ")).as_bytes())?;
470 self.write_summary_table(&mut f, duration, build_runner.bcx, error)?;
471 f.write_all(HTML_CANVAS.as_bytes())?;
472 self.write_unit_table(&mut f)?;
473 writeln!(
475 f,
476 "<script>\n\
477 DURATION = {};",
478 f64::ceil(duration) as u32
479 )?;
480 self.write_js_data(&mut f)?;
481 write!(
482 f,
483 "{}\n\
484 </script>\n\
485 </body>\n\
486 </html>\n\
487 ",
488 include_str!("timings.js")
489 )?;
490 drop(f);
491
492 let unstamped_filename = timings_path.join("cargo-timing.html");
493 paths::link_or_copy(&filename, &unstamped_filename)?;
494
495 let mut shell = self.gctx.shell();
496 let timing_path = std::env::current_dir().unwrap_or_default().join(&filename);
497 let link = shell.err_file_hyperlink(&timing_path);
498 let msg = format!("report saved to {link}{}{link:#}", timing_path.display(),);
499 shell.status_with_color("Timing", msg, &style::NOTE)?;
500
501 Ok(())
502 }
503
504 fn write_summary_table(
506 &self,
507 f: &mut impl Write,
508 duration: f64,
509 bcx: &BuildContext<'_, '_>,
510 error: &Option<anyhow::Error>,
511 ) -> CargoResult<()> {
512 let targets: Vec<String> = self
513 .root_targets
514 .iter()
515 .map(|(name, targets)| format!("{} ({})", name, targets.join(", ")))
516 .collect();
517 let targets = targets.join("<br>");
518 let time_human = if duration > 60.0 {
519 format!(" ({}m {:.1}s)", duration as u32 / 60, duration % 60.0)
520 } else {
521 "".to_string()
522 };
523 let total_time = format!("{:.1}s{}", duration, time_human);
524 let max_concurrency = self.concurrency.iter().map(|c| c.active).max().unwrap();
525 let num_cpus = available_parallelism()
526 .map(|x| x.get().to_string())
527 .unwrap_or_else(|_| "n/a".into());
528 let rustc_info = render_rustc_info(bcx);
529 let error_msg = match error {
530 Some(e) => format!(r#"<tr><td class="error-text">Error:</td><td>{e}</td></tr>"#),
531 None => "".to_string(),
532 };
533 write!(
534 f,
535 r#"
536<table class="my-table summary-table">
537 <tr>
538 <td>Targets:</td><td>{}</td>
539 </tr>
540 <tr>
541 <td>Profile:</td><td>{}</td>
542 </tr>
543 <tr>
544 <td>Fresh units:</td><td>{}</td>
545 </tr>
546 <tr>
547 <td>Dirty units:</td><td>{}</td>
548 </tr>
549 <tr>
550 <td>Total units:</td><td>{}</td>
551 </tr>
552 <tr>
553 <td>Max concurrency:</td><td>{} (jobs={} ncpu={})</td>
554 </tr>
555 <tr>
556 <td>Build start:</td><td>{}</td>
557 </tr>
558 <tr>
559 <td>Total time:</td><td>{}</td>
560 </tr>
561 <tr>
562 <td>rustc:</td><td>{}</td>
563 </tr>
564{}
565</table>
566"#,
567 targets,
568 self.profile,
569 self.total_fresh,
570 self.total_dirty,
571 self.total_fresh + self.total_dirty,
572 max_concurrency,
573 bcx.jobs(),
574 num_cpus,
575 self.start_str,
576 total_time,
577 rustc_info,
578 error_msg,
579 )?;
580 Ok(())
581 }
582
583 fn write_js_data(&self, f: &mut impl Write) -> CargoResult<()> {
586 let unit_map: HashMap<Unit, usize> = self
588 .unit_times
589 .iter()
590 .enumerate()
591 .map(|(i, ut)| (ut.unit.clone(), i))
592 .collect();
593 #[derive(serde::Serialize)]
594 struct UnitData {
595 i: usize,
596 name: String,
597 version: String,
598 mode: String,
599 target: String,
600 start: f64,
601 duration: f64,
602 rmeta_time: Option<f64>,
603 unlocked_units: Vec<usize>,
604 unlocked_rmeta_units: Vec<usize>,
605 sections: Option<Vec<(String, SectionData)>>,
606 }
607 let round = |x: f64| (x * 100.0).round() / 100.0;
608 let unit_data: Vec<UnitData> = self
609 .unit_times
610 .iter()
611 .enumerate()
612 .map(|(i, ut)| {
613 let mode = if ut.unit.mode.is_run_custom_build() {
614 "run-custom-build"
615 } else {
616 "todo"
617 }
618 .to_string();
619 let unlocked_units: Vec<usize> = ut
623 .unlocked_units
624 .iter()
625 .filter_map(|unit| unit_map.get(unit).copied())
626 .collect();
627 let unlocked_rmeta_units: Vec<usize> = ut
628 .unlocked_rmeta_units
629 .iter()
630 .filter_map(|unit| unit_map.get(unit).copied())
631 .collect();
632 let aggregated = ut.aggregate_sections();
633 let sections = match aggregated {
634 AggregatedSections::Sections(mut sections) => {
635 if let Some((_, section)) = sections.last()
642 && section.end < ut.duration
643 {
644 sections.push((
645 "other".to_string(),
646 SectionData {
647 start: section.end,
648 end: ut.duration,
649 },
650 ));
651 }
652
653 Some(sections)
654 }
655 AggregatedSections::OnlyMetadataTime { .. }
656 | AggregatedSections::OnlyTotalDuration => None,
657 };
658
659 UnitData {
660 i,
661 name: ut.unit.pkg.name().to_string(),
662 version: ut.unit.pkg.version().to_string(),
663 mode,
664 target: ut.target.clone(),
665 start: round(ut.start),
666 duration: round(ut.duration),
667 rmeta_time: ut.rmeta_time.map(round),
668 unlocked_units,
669 unlocked_rmeta_units,
670 sections,
671 }
672 })
673 .collect();
674 writeln!(
675 f,
676 "const UNIT_DATA = {};",
677 serde_json::to_string_pretty(&unit_data)?
678 )?;
679 writeln!(
680 f,
681 "const CONCURRENCY_DATA = {};",
682 serde_json::to_string_pretty(&self.concurrency)?
683 )?;
684 writeln!(
685 f,
686 "const CPU_USAGE = {};",
687 serde_json::to_string_pretty(&self.cpu_usage)?
688 )?;
689 Ok(())
690 }
691
692 fn write_unit_table(&self, f: &mut impl Write) -> CargoResult<()> {
694 let mut units: Vec<&UnitTime> = self.unit_times.iter().collect();
695 units.sort_unstable_by(|a, b| b.duration.partial_cmp(&a.duration).unwrap());
696
697 fn capitalize(s: &str) -> String {
700 let first_char = s
701 .chars()
702 .next()
703 .map(|c| c.to_uppercase().to_string())
704 .unwrap_or_default();
705 format!("{first_char}{}", s.chars().skip(1).collect::<String>())
706 }
707
708 let aggregated: Vec<AggregatedSections> = units
714 .iter()
715 .map(|u|
716 match u.aggregate_sections() {
720 AggregatedSections::Sections(sections) => AggregatedSections::Sections(
721 sections.into_iter()
722 .map(|(name, data)| (capitalize(&name), data))
723 .collect()
724 ),
725 s => s
726 })
727 .collect();
728
729 let headers: Vec<String> = if let Some(sections) = aggregated.iter().find_map(|s| match s {
730 AggregatedSections::Sections(sections) => Some(sections),
731 _ => None,
732 }) {
733 sections.into_iter().map(|s| s.0.clone()).collect()
734 } else if aggregated
735 .iter()
736 .any(|s| matches!(s, AggregatedSections::OnlyMetadataTime { .. }))
737 {
738 vec![
739 FRONTEND_SECTION_NAME.to_string(),
740 CODEGEN_SECTION_NAME.to_string(),
741 ]
742 } else {
743 vec![]
744 };
745
746 write!(
747 f,
748 r#"
749<table class="my-table">
750 <thead>
751 <tr>
752 <th></th>
753 <th>Unit</th>
754 <th>Total</th>
755 {headers}
756 <th>Features</th>
757 </tr>
758 </thead>
759 <tbody>
760"#,
761 headers = headers.iter().map(|h| format!("<th>{h}</th>")).join("\n")
762 )?;
763
764 for (i, (unit, aggregated_sections)) in units.iter().zip(aggregated).enumerate() {
765 let format_duration = |section: Option<SectionData>| match section {
766 Some(section) => {
767 let duration = section.duration();
768 let pct = (duration / unit.duration) * 100.0;
769 format!("{duration:.1}s ({:.0}%)", pct)
770 }
771 None => "".to_string(),
772 };
773
774 let mut cells: HashMap<&str, SectionData> = Default::default();
779
780 match &aggregated_sections {
781 AggregatedSections::Sections(sections) => {
782 for (name, data) in sections {
783 cells.insert(&name, *data);
784 }
785 }
786 AggregatedSections::OnlyMetadataTime { frontend, codegen } => {
787 cells.insert(FRONTEND_SECTION_NAME, *frontend);
788 cells.insert(CODEGEN_SECTION_NAME, *codegen);
789 }
790 AggregatedSections::OnlyTotalDuration => {}
791 };
792 let cells = headers
793 .iter()
794 .map(|header| {
795 format!(
796 "<td>{}</td>",
797 format_duration(cells.remove(header.as_str()))
798 )
799 })
800 .join("\n");
801
802 let features = unit.unit.features.join(", ");
803 write!(
804 f,
805 r#"
806<tr>
807 <td>{}.</td>
808 <td>{}{}</td>
809 <td>{:.1}s</td>
810 {cells}
811 <td>{features}</td>
812</tr>
813"#,
814 i + 1,
815 unit.name_ver(),
816 unit.target,
817 unit.duration,
818 )?;
819 }
820 write!(f, "</tbody>\n</table>\n")?;
821 Ok(())
822 }
823}
824
825impl UnitTime {
826 fn name_ver(&self) -> String {
827 format!("{} v{}", self.unit.pkg.name(), self.unit.pkg.version())
828 }
829
830 fn start_section(&mut self, name: &str, now: f64) {
831 if self
832 .sections
833 .insert(
834 name.to_string(),
835 CompilationSection {
836 start: now - self.start,
837 end: None,
838 },
839 )
840 .is_some()
841 {
842 warn!("compilation section {name} started more than once");
843 }
844 }
845
846 fn end_section(&mut self, name: &str, now: f64) {
847 let Some(section) = self.sections.get_mut(name) else {
848 warn!("compilation section {name} ended, but it has no start recorded");
849 return;
850 };
851 section.end = Some(now - self.start);
852 }
853}
854
855#[derive(serde::Deserialize, Debug)]
857#[serde(rename_all = "kebab-case")]
858pub enum SectionTimingEvent {
859 Start,
860 End,
861}
862
863#[derive(serde::Deserialize, Debug)]
866pub struct SectionTiming {
867 pub name: String,
868 pub event: SectionTimingEvent,
869}
870
871fn render_rustc_info(bcx: &BuildContext<'_, '_>) -> String {
872 let version = bcx
873 .rustc()
874 .verbose_version
875 .lines()
876 .next()
877 .expect("rustc version");
878 let requested_target = bcx
879 .build_config
880 .requested_kinds
881 .iter()
882 .map(|kind| bcx.target_data.short_name(kind))
883 .collect::<Vec<_>>()
884 .join(", ");
885 format!(
886 "{}<br>Host: {}<br>Target: {}",
887 version,
888 bcx.rustc().host,
889 requested_target
890 )
891}
892
893static HTML_TMPL: &str = r#"
894<html>
895<head>
896 <title>Cargo Build Timings — {ROOTS}</title>
897 <meta charset="utf-8">
898<style type="text/css">
899:root {
900 --error-text: #e80000;
901 --text: #000;
902 --background: #fff;
903 --h1-border-bottom: #c0c0c0;
904 --table-box-shadow: rgba(0, 0, 0, 0.1);
905 --table-th: #d5dde5;
906 --table-th-background: #1b1e24;
907 --table-th-border-bottom: #9ea7af;
908 --table-th-border-right: #343a45;
909 --table-tr-border-top: #c1c3d1;
910 --table-tr-border-bottom: #c1c3d1;
911 --table-tr-odd-background: #ebebeb;
912 --table-td-background: #ffffff;
913 --table-td-border-right: #C1C3D1;
914 --canvas-background: #f7f7f7;
915 --canvas-axes: #303030;
916 --canvas-grid: #e6e6e6;
917 --canvas-codegen: #aa95e8;
918 --canvas-link: #95e8aa;
919 --canvas-other: #e895aa;
920 --canvas-custom-build: #f0b165;
921 --canvas-not-custom-build: #95cce8;
922 --canvas-dep-line: #ddd;
923 --canvas-dep-line-highlighted: #000;
924 --canvas-cpu: rgba(250, 119, 0, 0.2);
925}
926
927@media (prefers-color-scheme: dark) {
928 :root {
929 --error-text: #e80000;
930 --text: #fff;
931 --background: #121212;
932 --h1-border-bottom: #444;
933 --table-box-shadow: rgba(255, 255, 255, 0.1);
934 --table-th: #a0a0a0;
935 --table-th-background: #2c2c2c;
936 --table-th-border-bottom: #555;
937 --table-th-border-right: #444;
938 --table-tr-border-top: #333;
939 --table-tr-border-bottom: #333;
940 --table-tr-odd-background: #1e1e1e;
941 --table-td-background: #262626;
942 --table-td-border-right: #333;
943 --canvas-background: #1a1a1a;
944 --canvas-axes: #b0b0b0;
945 --canvas-grid: #333;
946 --canvas-block: #aa95e8;
947 --canvas-custom-build: #f0b165;
948 --canvas-not-custom-build: #95cce8;
949 --canvas-dep-line: #444;
950 --canvas-dep-line-highlighted: #fff;
951 --canvas-cpu: rgba(250, 119, 0, 0.2);
952 }
953}
954
955html {
956 font-family: sans-serif;
957 color: var(--text);
958 background: var(--background);
959}
960
961.canvas-container {
962 position: relative;
963 margin-top: 5px;
964 margin-bottom: 5px;
965}
966
967h1 {
968 border-bottom: 1px solid var(--h1-border-bottom);
969}
970
971.graph {
972 display: block;
973}
974
975.my-table {
976 margin-top: 20px;
977 margin-bottom: 20px;
978 border-collapse: collapse;
979 box-shadow: 0 5px 10px var(--table-box-shadow);
980}
981
982.my-table th {
983 color: var(--table-th);
984 background: var(--table-th-background);
985 border-bottom: 4px solid var(--table-th-border-bottom);
986 border-right: 1px solid var(--table-th-border-right);
987 font-size: 18px;
988 font-weight: 100;
989 padding: 12px;
990 text-align: left;
991 vertical-align: middle;
992}
993
994.my-table th:first-child {
995 border-top-left-radius: 3px;
996}
997
998.my-table th:last-child {
999 border-top-right-radius: 3px;
1000 border-right:none;
1001}
1002
1003.my-table tr {
1004 border-top: 1px solid var(--table-tr-border-top);
1005 border-bottom: 1px solid var(--table-tr-border-bottom);
1006 font-size: 16px;
1007 font-weight: normal;
1008}
1009
1010.my-table tr:first-child {
1011 border-top:none;
1012}
1013
1014.my-table tr:last-child {
1015 border-bottom:none;
1016}
1017
1018.my-table tr:nth-child(odd) td {
1019 background: var(--table-tr-odd-background);
1020}
1021
1022.my-table tr:last-child td:first-child {
1023 border-bottom-left-radius:3px;
1024}
1025
1026.my-table tr:last-child td:last-child {
1027 border-bottom-right-radius:3px;
1028}
1029
1030.my-table td {
1031 background: var(--table-td-background);
1032 padding: 10px;
1033 text-align: left;
1034 vertical-align: middle;
1035 font-weight: 300;
1036 font-size: 14px;
1037 border-right: 1px solid var(--table-td-border-right);
1038}
1039
1040.my-table td:last-child {
1041 border-right: 0px;
1042}
1043
1044.summary-table td:first-child {
1045 vertical-align: top;
1046 text-align: right;
1047}
1048
1049.input-table td {
1050 text-align: center;
1051}
1052
1053.error-text {
1054 color: var(--error-text);
1055}
1056
1057</style>
1058</head>
1059<body>
1060
1061<h1>Cargo Build Timings</h1>
1062See <a href="https://doc.rust-lang.org/nightly/cargo/reference/timings.html">Documentation</a>
1063"#;
1064
1065static HTML_CANVAS: &str = r#"
1066<table class="input-table">
1067 <tr>
1068 <td><label for="min-unit-time">Min unit time:</label></td>
1069 <td title="Scale corresponds to a number of pixels per second. It is automatically initialized based on your viewport width.">
1070 <label for="scale">Scale:</label>
1071 </td>
1072 </tr>
1073 <tr>
1074 <td><input type="range" min="0" max="30" step="0.1" value="0" id="min-unit-time"></td>
1075 <!--
1076 The scale corresponds to some number of "pixels per second".
1077 Its min, max, and initial values are automatically set by JavaScript on page load,
1078 based on the client viewport.
1079 -->
1080 <td><input type="range" min="1" max="100" value="50" id="scale"></td>
1081 </tr>
1082 <tr>
1083 <td><output for="min-unit-time" id="min-unit-time-output"></output></td>
1084 <td><output for="scale" id="scale-output"></output></td>
1085 </tr>
1086</table>
1087
1088<div id="pipeline-container" class="canvas-container">
1089 <canvas id="pipeline-graph" class="graph" style="position: absolute; left: 0; top: 0; z-index: 0;"></canvas>
1090 <canvas id="pipeline-graph-lines" style="position: absolute; left: 0; top: 0; z-index: 1; pointer-events:none;"></canvas>
1091</div>
1092<div class="canvas-container">
1093 <canvas id="timing-graph" class="graph"></canvas>
1094</div>
1095"#;