cargo/core/compiler/
timings.rs

1//! Timing tracking.
2//!
3//! This module implements some simple tracking information for timing of how
4//! long it takes for different units to compile.
5use super::{CompileMode, Unit};
6use crate::core::PackageId;
7use crate::core::compiler::job_queue::JobId;
8use crate::core::compiler::{BuildContext, BuildRunner, TimingOutput};
9use crate::util::cpu::State;
10use crate::util::machine_message::{self, Message};
11use crate::util::style;
12use crate::util::{CargoResult, GlobalContext};
13use anyhow::Context as _;
14use cargo_util::paths;
15use 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
23/// Tracking information for the entire build.
24///
25/// Methods on this structure are generally called from the main thread of a
26/// running [`JobQueue`] instance (`DrainState` in specific) when the queue
27/// receives messages from spawned off threads.
28///
29/// [`JobQueue`]: super::JobQueue
30pub struct Timings<'gctx> {
31    gctx: &'gctx GlobalContext,
32    /// Whether or not timings should be captured.
33    enabled: bool,
34    /// If true, saves an HTML report to disk.
35    report_html: bool,
36    /// If true, emits JSON information with timing information.
37    report_json: bool,
38    /// When Cargo started.
39    start: Instant,
40    /// A rendered string of when compilation started.
41    start_str: String,
42    /// A summary of the root units.
43    ///
44    /// Tuples of `(package_description, target_descriptions)`.
45    root_targets: Vec<(String, Vec<String>)>,
46    /// The build profile.
47    profile: String,
48    /// Total number of fresh units.
49    total_fresh: u32,
50    /// Total number of dirty units.
51    total_dirty: u32,
52    /// Time tracking for each individual unit.
53    unit_times: Vec<UnitTime>,
54    /// Units that are in the process of being built.
55    /// When they finished, they are moved to `unit_times`.
56    active: HashMap<JobId, UnitTime>,
57    /// Concurrency-tracking information. This is periodically updated while
58    /// compilation progresses.
59    concurrency: Vec<Concurrency>,
60    /// Last recorded state of the system's CPUs and when it happened
61    last_cpu_state: Option<State>,
62    last_cpu_recording: Instant,
63    /// Recorded CPU states, stored as tuples. First element is when the
64    /// recording was taken and second element is percentage usage of the
65    /// system.
66    cpu_usage: Vec<(f64, f64)>,
67}
68
69/// Section of compilation (e.g. frontend, backend, linking).
70#[derive(Copy, Clone, serde::Serialize)]
71pub struct CompilationSection {
72    /// Start of the section, as an offset in seconds from `UnitTime::start`.
73    start: f64,
74    /// End of the section, as an offset in seconds from `UnitTime::start`.
75    end: Option<f64>,
76}
77
78/// Tracking information for an individual unit.
79struct UnitTime {
80    unit: Unit,
81    /// A string describing the cargo target.
82    target: String,
83    /// The time when this unit started as an offset in seconds from `Timings::start`.
84    start: f64,
85    /// Total time to build this unit in seconds.
86    duration: f64,
87    /// The time when the `.rmeta` file was generated, an offset in seconds
88    /// from `start`.
89    rmeta_time: Option<f64>,
90    /// Reverse deps that are freed to run after this unit finished.
91    unlocked_units: Vec<Unit>,
92    /// Same as `unlocked_units`, but unlocked by rmeta.
93    unlocked_rmeta_units: Vec<Unit>,
94    /// Individual compilation section durations, gathered from `--json=timings`.
95    ///
96    /// IndexMap is used to keep original insertion order, we want to be able to tell which
97    /// sections were started in which order.
98    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            // We have some detailed compilation section timings, so we postprocess them
110            // Since it is possible that we do not have an end timestamp for a given compilation
111            // section, we need to iterate them and if an end is missing, we assign the end of
112            // the section to the start of the following section.
113
114            let mut sections = vec![];
115
116            // The frontend section is currently implicit in rustc, it is assumed to start at
117            // compilation start and end when codegen starts. So we hard-code it here.
118            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                // Store the previous section, potentially setting its end to the start of the
127                // current section.
128                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            // Store the last section, potentially setting its end to the end of the whole
138            // compilation.
139            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            // We only know when the rmeta time was generated
150            AggregatedSections::OnlyMetadataTime {
151                frontend: SectionData {
152                    start: 0.0,
153                    end: rmeta,
154                },
155                codegen: SectionData { start: rmeta, end },
156            }
157        } else {
158            // We only know the total duration
159            AggregatedSections::OnlyTotalDuration
160        }
161    }
162}
163
164/// Periodic concurrency tracking information.
165#[derive(serde::Serialize)]
166struct Concurrency {
167    /// Time as an offset in seconds from `Timings::start`.
168    t: f64,
169    /// Number of units currently running.
170    active: usize,
171    /// Number of units that could run, but are waiting for a jobserver token.
172    waiting: usize,
173    /// Number of units that are not yet ready, because they are waiting for
174    /// dependencies to finish.
175    inactive: usize,
176}
177
178/// Postprocessed section data that has both start and an end.
179#[derive(Copy, Clone, serde::Serialize)]
180struct SectionData {
181    /// Start (relative to the start of the unit)
182    start: f64,
183    /// End (relative to the start of the unit)
184    end: f64,
185}
186
187impl SectionData {
188    fn duration(&self) -> f64 {
189        (self.end - self.start).max(0.0)
190    }
191}
192
193/// Contains post-processed data of individual compilation sections.
194enum AggregatedSections {
195    /// We know the names and durations of individual compilation sections
196    Sections(Vec<(String, SectionData)>),
197    /// We only know when .rmeta was generated, so we can distill frontend and codegen time.
198    OnlyMetadataTime {
199        frontend: SectionData,
200        codegen: SectionData,
201    },
202    /// We know only the total duration
203    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    /// Mark that a unit has started running.
263    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            // Special case for brevity, since most dependencies hit
269            // this path.
270            "".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    /// Mark that the `.rmeta` file as generated.
298    pub fn unit_rmeta_finished(&mut self, id: JobId, unlocked: Vec<&Unit>) {
299        if !self.enabled {
300            return;
301        }
302        // `id` may not always be active. "fresh" units unconditionally
303        // generate `Message::Finish`, but this active map only tracks dirty
304        // units.
305        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    /// Mark that a unit has finished running.
317    pub fn unit_finished(&mut self, id: JobId, unlocked: Vec<&Unit>) {
318        if !self.enabled {
319            return;
320        }
321        // See note above in `unit_rmeta_finished`, this may not always be active.
322        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    /// Handle the start/end of a compilation section.
347    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(&section_timing.name, now);
359            }
360            SectionTimingEvent::End => {
361                unit_time.end_section(&section_timing.name, now);
362            }
363        }
364    }
365
366    /// This is called periodically to mark the concurrency of internal structures.
367    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    /// Mark that a fresh unit was encountered. (No re-compile needed)
381    pub fn add_fresh(&mut self) {
382        self.total_fresh += 1;
383    }
384
385    /// Mark that a dirty unit was encountered. (Re-compile needed)
386    pub fn add_dirty(&mut self) {
387        self.total_dirty += 1;
388    }
389
390    /// Take a sample of CPU usage
391    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        // Don't take samples too frequently, even if requested.
399        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    /// Call this when all units are finished.
418    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    /// Save HTML report to disk.
437    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        // It helps with pixel alignment to use whole numbers.
458        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    /// Render the summary table.
489    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    /// Write timing data in JavaScript. Primarily for `timings.js` to put data
568    /// in a `<script>` HTML element to draw graphs.
569    fn write_js_data(&self, f: &mut impl Write) -> CargoResult<()> {
570        // Create a map to link indices of unlocked units.
571        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                // These filter on the unlocked units because not all unlocked
604                // units are actually "built". For example, Doctest mode units
605                // don't actually generate artifacts.
606                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                        // We draw the sections in the pipeline graph in a way where the frontend
620                        // section has the "default" build color, and then additional sections
621                        // (codegen, link) are overlayed on top with a different color.
622                        // However, there might be some time after the final (usually link) section,
623                        // which definitely shouldn't be classified as "Frontend". We thus try to
624                        // detect this situation and add a final "Other" section.
625                        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    /// Render the table of all units.
677    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        // Make the first "letter" uppercase. We could probably just assume ASCII here, but this
682        // should be Unicode compatible.
683        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        // We can have a bunch of situations here.
693        // - -Zsection-timings is enabled, and we received some custom sections, in which
694        // case we use them to determine the headers.
695        // - We have at least one rmeta time, so we hard-code Frontend and Codegen headers.
696        // - We only have total durations, so we don't add any additional headers.
697        let aggregated: Vec<AggregatedSections> = units
698            .iter()
699            .map(|u|
700                // Normalize the section names so that they are capitalized, so that we can later
701                // refer to them with the capitalized name both when computing headers and when
702                // looking up cells.
703                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            // This is a bit complex, as we assume the most general option - we can have an
759            // arbitrary set of headers, and an arbitrary set of sections per unit, so we always
760            // initiate the cells to be empty, and then try to find a corresponding column for which
761            // we might have data.
762            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/// Start or end of a section timing.
840#[derive(serde::Deserialize, Debug)]
841#[serde(rename_all = "kebab-case")]
842pub enum SectionTimingEvent {
843    Start,
844    End,
845}
846
847/// Represents a certain section (phase) of rustc compilation.
848/// It is emitted by rustc when the `--json=timings` flag is used.
849#[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"#;