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::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
24/// Tracking information for the entire build.
25///
26/// Methods on this structure are generally called from the main thread of a
27/// running [`JobQueue`] instance (`DrainState` in specific) when the queue
28/// receives messages from spawned off threads.
29///
30/// [`JobQueue`]: super::JobQueue
31pub struct Timings<'gctx> {
32    gctx: &'gctx GlobalContext,
33    /// Whether or not timings should be captured.
34    enabled: bool,
35    /// If true, saves an HTML report to disk.
36    report_html: bool,
37    /// If true, emits JSON information with timing information.
38    report_json: bool,
39    /// When Cargo started.
40    start: Instant,
41    /// A rendered string of when compilation started.
42    start_str: String,
43    /// A summary of the root units.
44    ///
45    /// Tuples of `(package_description, target_descriptions)`.
46    root_targets: Vec<(String, Vec<String>)>,
47    /// The build profile.
48    profile: String,
49    /// Total number of fresh units.
50    total_fresh: u32,
51    /// Total number of dirty units.
52    total_dirty: u32,
53    /// Time tracking for each individual unit.
54    unit_times: Vec<UnitTime>,
55    /// Units that are in the process of being built.
56    /// When they finished, they are moved to `unit_times`.
57    active: HashMap<JobId, UnitTime>,
58    /// Concurrency-tracking information. This is periodically updated while
59    /// compilation progresses.
60    concurrency: Vec<Concurrency>,
61    /// Last recorded state of the system's CPUs and when it happened
62    last_cpu_state: Option<State>,
63    last_cpu_recording: Instant,
64    /// Recorded CPU states, stored as tuples. First element is when the
65    /// recording was taken and second element is percentage usage of the
66    /// system.
67    cpu_usage: Vec<(f64, f64)>,
68}
69
70/// Section of compilation (e.g. frontend, backend, linking).
71#[derive(Copy, Clone, serde::Serialize)]
72pub struct CompilationSection {
73    /// Start of the section, as an offset in seconds from `UnitTime::start`.
74    start: f64,
75    /// End of the section, as an offset in seconds from `UnitTime::start`.
76    end: Option<f64>,
77}
78
79/// Tracking information for an individual unit.
80struct UnitTime {
81    unit: Unit,
82    /// A string describing the cargo target.
83    target: String,
84    /// The time when this unit started as an offset in seconds from `Timings::start`.
85    start: f64,
86    /// Total time to build this unit in seconds.
87    duration: f64,
88    /// The time when the `.rmeta` file was generated, an offset in seconds
89    /// from `start`.
90    rmeta_time: Option<f64>,
91    /// Reverse deps that are freed to run after this unit finished.
92    unlocked_units: Vec<Unit>,
93    /// Same as `unlocked_units`, but unlocked by rmeta.
94    unlocked_rmeta_units: Vec<Unit>,
95    /// Individual compilation section durations, gathered from `--json=timings`.
96    ///
97    /// IndexMap is used to keep original insertion order, we want to be able to tell which
98    /// sections were started in which order.
99    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            // We have some detailed compilation section timings, so we postprocess them
111            // Since it is possible that we do not have an end timestamp for a given compilation
112            // section, we need to iterate them and if an end is missing, we assign the end of
113            // the section to the start of the following section.
114
115            let mut sections = vec![];
116
117            // The frontend section is currently implicit in rustc, it is assumed to start at
118            // compilation start and end when codegen starts. So we hard-code it here.
119            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                // Store the previous section, potentially setting its end to the start of the
128                // current section.
129                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            // Store the last section, potentially setting its end to the end of the whole
139            // compilation.
140            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            // We only know when the rmeta time was generated
151            AggregatedSections::OnlyMetadataTime {
152                frontend: SectionData {
153                    start: 0.0,
154                    end: rmeta,
155                },
156                codegen: SectionData { start: rmeta, end },
157            }
158        } else {
159            // We only know the total duration
160            AggregatedSections::OnlyTotalDuration
161        }
162    }
163}
164
165/// Periodic concurrency tracking information.
166#[derive(serde::Serialize)]
167struct Concurrency {
168    /// Time as an offset in seconds from `Timings::start`.
169    t: f64,
170    /// Number of units currently running.
171    active: usize,
172    /// Number of units that could run, but are waiting for a jobserver token.
173    waiting: usize,
174    /// Number of units that are not yet ready, because they are waiting for
175    /// dependencies to finish.
176    inactive: usize,
177}
178
179/// Postprocessed section data that has both start and an end.
180#[derive(Copy, Clone, serde::Serialize)]
181struct SectionData {
182    /// Start (relative to the start of the unit)
183    start: f64,
184    /// End (relative to the start of the unit)
185    end: f64,
186}
187
188impl SectionData {
189    fn duration(&self) -> f64 {
190        (self.end - self.start).max(0.0)
191    }
192}
193
194/// Contains post-processed data of individual compilation sections.
195enum AggregatedSections {
196    /// We know the names and durations of individual compilation sections
197    Sections(Vec<(String, SectionData)>),
198    /// We only know when .rmeta was generated, so we can distill frontend and codegen time.
199    OnlyMetadataTime {
200        frontend: SectionData,
201        codegen: SectionData,
202    },
203    /// We know only the total duration
204    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    /// Mark that a unit has started running.
264    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            // Special case for brevity, since most dependencies hit
270            // this path.
271            "".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    /// Mark that the `.rmeta` file as generated.
299    pub fn unit_rmeta_finished(&mut self, id: JobId, unlocked: Vec<&Unit>) {
300        if !self.enabled {
301            return;
302        }
303        // `id` may not always be active. "fresh" units unconditionally
304        // generate `Message::Finish`, but this active map only tracks dirty
305        // units.
306        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    /// Mark that a unit has finished running.
318    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        // See note above in `unit_rmeta_finished`, this may not always be active.
328        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    /// Handle the start/end of a compilation section.
363    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(&section_timing.name, now);
375            }
376            SectionTimingEvent::End => {
377                unit_time.end_section(&section_timing.name, now);
378            }
379        }
380    }
381
382    /// This is called periodically to mark the concurrency of internal structures.
383    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    /// Mark that a fresh unit was encountered. (No re-compile needed)
397    pub fn add_fresh(&mut self) {
398        self.total_fresh += 1;
399    }
400
401    /// Mark that a dirty unit was encountered. (Re-compile needed)
402    pub fn add_dirty(&mut self) {
403        self.total_dirty += 1;
404    }
405
406    /// Take a sample of CPU usage
407    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        // Don't take samples too frequently, even if requested.
415        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    /// Call this when all units are finished.
434    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    /// Save HTML report to disk.
453    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        // It helps with pixel alignment to use whole numbers.
474        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    /// Render the summary table.
505    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    /// Write timing data in JavaScript. Primarily for `timings.js` to put data
584    /// in a `<script>` HTML element to draw graphs.
585    fn write_js_data(&self, f: &mut impl Write) -> CargoResult<()> {
586        // Create a map to link indices of unlocked units.
587        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                // These filter on the unlocked units because not all unlocked
620                // units are actually "built". For example, Doctest mode units
621                // don't actually generate artifacts.
622                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                        // We draw the sections in the pipeline graph in a way where the frontend
636                        // section has the "default" build color, and then additional sections
637                        // (codegen, link) are overlayed on top with a different color.
638                        // However, there might be some time after the final (usually link) section,
639                        // which definitely shouldn't be classified as "Frontend". We thus try to
640                        // detect this situation and add a final "Other" section.
641                        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    /// Render the table of all units.
693    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        // Make the first "letter" uppercase. We could probably just assume ASCII here, but this
698        // should be Unicode compatible.
699        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        // We can have a bunch of situations here.
709        // - -Zsection-timings is enabled, and we received some custom sections, in which
710        // case we use them to determine the headers.
711        // - We have at least one rmeta time, so we hard-code Frontend and Codegen headers.
712        // - We only have total durations, so we don't add any additional headers.
713        let aggregated: Vec<AggregatedSections> = units
714            .iter()
715            .map(|u|
716                // Normalize the section names so that they are capitalized, so that we can later
717                // refer to them with the capitalized name both when computing headers and when
718                // looking up cells.
719                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            // This is a bit complex, as we assume the most general option - we can have an
775            // arbitrary set of headers, and an arbitrary set of sections per unit, so we always
776            // initiate the cells to be empty, and then try to find a corresponding column for which
777            // we might have data.
778            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/// Start or end of a section timing.
856#[derive(serde::Deserialize, Debug)]
857#[serde(rename_all = "kebab-case")]
858pub enum SectionTimingEvent {
859    Start,
860    End,
861}
862
863/// Represents a certain section (phase) of rustc compilation.
864/// It is emitted by rustc when the `--json=timings` flag is used.
865#[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"#;