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.
194#[derive(serde::Serialize)]
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;
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(&mut self, id: JobId, unlocked: Vec<&Unit>) {
319        if !self.enabled {
320            return;
321        }
322        // See note above in `unit_rmeta_finished`, this may not always be active.
323        let Some(mut unit_time) = self.active.remove(&id) else {
324            return;
325        };
326        let t = self.start.elapsed().as_secs_f64();
327        unit_time.duration = t - unit_time.start;
328        assert!(unit_time.unlocked_units.is_empty());
329        unit_time
330            .unlocked_units
331            .extend(unlocked.iter().cloned().cloned());
332        if self.report_json {
333            let msg = machine_message::TimingInfo {
334                package_id: unit_time.unit.pkg.package_id().to_spec(),
335                target: &unit_time.unit.target,
336                mode: unit_time.unit.mode,
337                duration: unit_time.duration,
338                rmeta_time: unit_time.rmeta_time,
339                sections: unit_time.sections.clone().into_iter().collect(),
340            }
341            .to_json_string();
342            crate::drop_println!(self.gctx, "{}", msg);
343        }
344        self.unit_times.push(unit_time);
345    }
346
347    /// Handle the start/end of a compilation section.
348    pub fn unit_section_timing(&mut self, id: JobId, section_timing: &SectionTiming) {
349        if !self.enabled {
350            return;
351        }
352        let Some(unit_time) = self.active.get_mut(&id) else {
353            return;
354        };
355        let now = self.start.elapsed().as_secs_f64();
356
357        match section_timing.event {
358            SectionTimingEvent::Start => {
359                unit_time.start_section(&section_timing.name, now);
360            }
361            SectionTimingEvent::End => {
362                unit_time.end_section(&section_timing.name, now);
363            }
364        }
365    }
366
367    /// This is called periodically to mark the concurrency of internal structures.
368    pub fn mark_concurrency(&mut self, active: usize, waiting: usize, inactive: usize) {
369        if !self.enabled {
370            return;
371        }
372        let c = Concurrency {
373            t: self.start.elapsed().as_secs_f64(),
374            active,
375            waiting,
376            inactive,
377        };
378        self.concurrency.push(c);
379    }
380
381    /// Mark that a fresh unit was encountered. (No re-compile needed)
382    pub fn add_fresh(&mut self) {
383        self.total_fresh += 1;
384    }
385
386    /// Mark that a dirty unit was encountered. (Re-compile needed)
387    pub fn add_dirty(&mut self) {
388        self.total_dirty += 1;
389    }
390
391    /// Take a sample of CPU usage
392    pub fn record_cpu(&mut self) {
393        if !self.enabled {
394            return;
395        }
396        let Some(prev) = &mut self.last_cpu_state else {
397            return;
398        };
399        // Don't take samples too frequently, even if requested.
400        let now = Instant::now();
401        if self.last_cpu_recording.elapsed() < Duration::from_millis(100) {
402            return;
403        }
404        let current = match State::current() {
405            Ok(s) => s,
406            Err(e) => {
407                tracing::info!("failed to get CPU state: {:?}", e);
408                return;
409            }
410        };
411        let pct_idle = current.idle_since(prev);
412        *prev = current;
413        self.last_cpu_recording = now;
414        let dur = now.duration_since(self.start).as_secs_f64();
415        self.cpu_usage.push((dur, 100.0 - pct_idle));
416    }
417
418    /// Call this when all units are finished.
419    pub fn finished(
420        &mut self,
421        build_runner: &BuildRunner<'_, '_>,
422        error: &Option<anyhow::Error>,
423    ) -> CargoResult<()> {
424        if !self.enabled {
425            return Ok(());
426        }
427        self.mark_concurrency(0, 0, 0);
428        self.unit_times
429            .sort_unstable_by(|a, b| a.start.partial_cmp(&b.start).unwrap());
430        if self.report_html {
431            self.report_html(build_runner, error)
432                .context("failed to save timing report")?;
433        }
434        Ok(())
435    }
436
437    /// Save HTML report to disk.
438    fn report_html(
439        &self,
440        build_runner: &BuildRunner<'_, '_>,
441        error: &Option<anyhow::Error>,
442    ) -> CargoResult<()> {
443        let duration = self.start.elapsed().as_secs_f64();
444        let timestamp = self.start_str.replace(&['-', ':'][..], "");
445        let timings_path = build_runner.files().host_root().join("cargo-timings");
446        paths::create_dir_all(&timings_path)?;
447        let filename = timings_path.join(format!("cargo-timing-{}.html", timestamp));
448        let mut f = BufWriter::new(paths::create(&filename)?);
449        let roots: Vec<&str> = self
450            .root_targets
451            .iter()
452            .map(|(name, _targets)| name.as_str())
453            .collect();
454        f.write_all(HTML_TMPL.replace("{ROOTS}", &roots.join(", ")).as_bytes())?;
455        self.write_summary_table(&mut f, duration, build_runner.bcx, error)?;
456        f.write_all(HTML_CANVAS.as_bytes())?;
457        self.write_unit_table(&mut f)?;
458        // It helps with pixel alignment to use whole numbers.
459        writeln!(
460            f,
461            "<script>\n\
462             DURATION = {};",
463            f64::ceil(duration) as u32
464        )?;
465        self.write_js_data(&mut f)?;
466        write!(
467            f,
468            "{}\n\
469             </script>\n\
470             </body>\n\
471             </html>\n\
472             ",
473            include_str!("timings.js")
474        )?;
475        drop(f);
476
477        let unstamped_filename = timings_path.join("cargo-timing.html");
478        paths::link_or_copy(&filename, &unstamped_filename)?;
479
480        let mut shell = self.gctx.shell();
481        let timing_path = std::env::current_dir().unwrap_or_default().join(&filename);
482        let link = shell.err_file_hyperlink(&timing_path);
483        let msg = format!("report saved to {link}{}{link:#}", timing_path.display(),);
484        shell.status_with_color("Timing", msg, &style::NOTE)?;
485
486        Ok(())
487    }
488
489    /// Render the summary table.
490    fn write_summary_table(
491        &self,
492        f: &mut impl Write,
493        duration: f64,
494        bcx: &BuildContext<'_, '_>,
495        error: &Option<anyhow::Error>,
496    ) -> CargoResult<()> {
497        let targets: Vec<String> = self
498            .root_targets
499            .iter()
500            .map(|(name, targets)| format!("{} ({})", name, targets.join(", ")))
501            .collect();
502        let targets = targets.join("<br>");
503        let time_human = if duration > 60.0 {
504            format!(" ({}m {:.1}s)", duration as u32 / 60, duration % 60.0)
505        } else {
506            "".to_string()
507        };
508        let total_time = format!("{:.1}s{}", duration, time_human);
509        let max_concurrency = self.concurrency.iter().map(|c| c.active).max().unwrap();
510        let num_cpus = available_parallelism()
511            .map(|x| x.get().to_string())
512            .unwrap_or_else(|_| "n/a".into());
513        let rustc_info = render_rustc_info(bcx);
514        let error_msg = match error {
515            Some(e) => format!(r#"<tr><td class="error-text">Error:</td><td>{e}</td></tr>"#),
516            None => "".to_string(),
517        };
518        write!(
519            f,
520            r#"
521<table class="my-table summary-table">
522  <tr>
523    <td>Targets:</td><td>{}</td>
524  </tr>
525  <tr>
526    <td>Profile:</td><td>{}</td>
527  </tr>
528  <tr>
529    <td>Fresh units:</td><td>{}</td>
530  </tr>
531  <tr>
532    <td>Dirty units:</td><td>{}</td>
533  </tr>
534  <tr>
535    <td>Total units:</td><td>{}</td>
536  </tr>
537  <tr>
538    <td>Max concurrency:</td><td>{} (jobs={} ncpu={})</td>
539  </tr>
540  <tr>
541    <td>Build start:</td><td>{}</td>
542  </tr>
543  <tr>
544    <td>Total time:</td><td>{}</td>
545  </tr>
546  <tr>
547    <td>rustc:</td><td>{}</td>
548  </tr>
549{}
550</table>
551"#,
552            targets,
553            self.profile,
554            self.total_fresh,
555            self.total_dirty,
556            self.total_fresh + self.total_dirty,
557            max_concurrency,
558            bcx.jobs(),
559            num_cpus,
560            self.start_str,
561            total_time,
562            rustc_info,
563            error_msg,
564        )?;
565        Ok(())
566    }
567
568    /// Write timing data in JavaScript. Primarily for `timings.js` to put data
569    /// in a `<script>` HTML element to draw graphs.
570    fn write_js_data(&self, f: &mut impl Write) -> CargoResult<()> {
571        // Create a map to link indices of unlocked units.
572        let unit_map: HashMap<Unit, usize> = self
573            .unit_times
574            .iter()
575            .enumerate()
576            .map(|(i, ut)| (ut.unit.clone(), i))
577            .collect();
578        #[derive(serde::Serialize)]
579        struct UnitData {
580            i: usize,
581            name: String,
582            version: String,
583            mode: String,
584            target: String,
585            start: f64,
586            duration: f64,
587            rmeta_time: Option<f64>,
588            unlocked_units: Vec<usize>,
589            unlocked_rmeta_units: Vec<usize>,
590        }
591        let round = |x: f64| (x * 100.0).round() / 100.0;
592        let unit_data: Vec<UnitData> = self
593            .unit_times
594            .iter()
595            .enumerate()
596            .map(|(i, ut)| {
597                let mode = if ut.unit.mode.is_run_custom_build() {
598                    "run-custom-build"
599                } else {
600                    "todo"
601                }
602                .to_string();
603
604                // These filter on the unlocked units because not all unlocked
605                // units are actually "built". For example, Doctest mode units
606                // don't actually generate artifacts.
607                let unlocked_units: Vec<usize> = ut
608                    .unlocked_units
609                    .iter()
610                    .filter_map(|unit| unit_map.get(unit).copied())
611                    .collect();
612                let unlocked_rmeta_units: Vec<usize> = ut
613                    .unlocked_rmeta_units
614                    .iter()
615                    .filter_map(|unit| unit_map.get(unit).copied())
616                    .collect();
617                UnitData {
618                    i,
619                    name: ut.unit.pkg.name().to_string(),
620                    version: ut.unit.pkg.version().to_string(),
621                    mode,
622                    target: ut.target.clone(),
623                    start: round(ut.start),
624                    duration: round(ut.duration),
625                    rmeta_time: ut.rmeta_time.map(round),
626                    unlocked_units,
627                    unlocked_rmeta_units,
628                }
629            })
630            .collect();
631        writeln!(
632            f,
633            "const UNIT_DATA = {};",
634            serde_json::to_string_pretty(&unit_data)?
635        )?;
636        writeln!(
637            f,
638            "const CONCURRENCY_DATA = {};",
639            serde_json::to_string_pretty(&self.concurrency)?
640        )?;
641        writeln!(
642            f,
643            "const CPU_USAGE = {};",
644            serde_json::to_string_pretty(&self.cpu_usage)?
645        )?;
646        Ok(())
647    }
648
649    /// Render the table of all units.
650    fn write_unit_table(&self, f: &mut impl Write) -> CargoResult<()> {
651        let mut units: Vec<&UnitTime> = self.unit_times.iter().collect();
652        units.sort_unstable_by(|a, b| b.duration.partial_cmp(&a.duration).unwrap());
653
654        // Make the first "letter" uppercase. We could probably just assume ASCII here, but this
655        // should be Unicode compatible.
656        fn capitalize(s: &str) -> String {
657            let first_char = s
658                .chars()
659                .next()
660                .map(|c| c.to_uppercase().to_string())
661                .unwrap_or_default();
662            format!("{first_char}{}", s.chars().skip(1).collect::<String>())
663        }
664
665        // We can have a bunch of situations here.
666        // - -Zsection-timings is enabled, and we received some custom sections, in which
667        // case we use them to determine the headers.
668        // - We have at least one rmeta time, so we hard-code Frontend and Codegen headers.
669        // - We only have total durations, so we don't add any additional headers.
670        let aggregated: Vec<AggregatedSections> = units
671            .iter()
672            .map(|u|
673                // Normalize the section names so that they are capitalized, so that we can later
674                // refer to them with the capitalized name both when computing headers and when
675                // looking up cells.
676                match u.aggregate_sections() {
677                    AggregatedSections::Sections(sections) => AggregatedSections::Sections(
678                        sections.into_iter()
679                            .map(|(name, data)| (capitalize(&name), data))
680                            .collect()
681                    ),
682                    s => s
683                })
684            .collect();
685
686        let headers: Vec<String> = if let Some(sections) = aggregated.iter().find_map(|s| match s {
687            AggregatedSections::Sections(sections) => Some(sections),
688            _ => None,
689        }) {
690            sections.into_iter().map(|s| s.0.clone()).collect()
691        } else if aggregated
692            .iter()
693            .any(|s| matches!(s, AggregatedSections::OnlyMetadataTime { .. }))
694        {
695            vec![
696                FRONTEND_SECTION_NAME.to_string(),
697                CODEGEN_SECTION_NAME.to_string(),
698            ]
699        } else {
700            vec![]
701        };
702
703        write!(
704            f,
705            r#"
706<table class="my-table">
707  <thead>
708    <tr>
709      <th></th>
710      <th>Unit</th>
711      <th>Total</th>
712      {headers}
713      <th>Features</th>
714    </tr>
715  </thead>
716  <tbody>
717"#,
718            headers = headers.iter().map(|h| format!("<th>{h}</th>")).join("\n")
719        )?;
720
721        for (i, (unit, aggregated_sections)) in units.iter().zip(aggregated).enumerate() {
722            let format_duration = |section: Option<SectionData>| match section {
723                Some(section) => {
724                    let duration = section.duration();
725                    let pct = (duration / unit.duration) * 100.0;
726                    format!("{duration:.1}s ({:.0}%)", pct)
727                }
728                None => "".to_string(),
729            };
730
731            // This is a bit complex, as we assume the most general option - we can have an
732            // arbitrary set of headers, and an arbitrary set of sections per unit, so we always
733            // initiate the cells to be empty, and then try to find a corresponding column for which
734            // we might have data.
735            let mut cells: HashMap<&str, SectionData> = Default::default();
736
737            match &aggregated_sections {
738                AggregatedSections::Sections(sections) => {
739                    for (name, data) in sections {
740                        cells.insert(&name, *data);
741                    }
742                }
743                AggregatedSections::OnlyMetadataTime { frontend, codegen } => {
744                    cells.insert(FRONTEND_SECTION_NAME, *frontend);
745                    cells.insert(CODEGEN_SECTION_NAME, *codegen);
746                }
747                AggregatedSections::OnlyTotalDuration => {}
748            };
749            let cells = headers
750                .iter()
751                .map(|header| {
752                    format!(
753                        "<td>{}</td>",
754                        format_duration(cells.remove(header.as_str()))
755                    )
756                })
757                .join("\n");
758
759            let features = unit.unit.features.join(", ");
760            write!(
761                f,
762                r#"
763<tr>
764  <td>{}.</td>
765  <td>{}{}</td>
766  <td>{:.1}s</td>
767  {cells}
768  <td>{features}</td>
769</tr>
770"#,
771                i + 1,
772                unit.name_ver(),
773                unit.target,
774                unit.duration,
775            )?;
776        }
777        write!(f, "</tbody>\n</table>\n")?;
778        Ok(())
779    }
780}
781
782impl UnitTime {
783    fn name_ver(&self) -> String {
784        format!("{} v{}", self.unit.pkg.name(), self.unit.pkg.version())
785    }
786
787    fn start_section(&mut self, name: &str, now: f64) {
788        if self
789            .sections
790            .insert(
791                name.to_string(),
792                CompilationSection {
793                    start: now - self.start,
794                    end: None,
795                },
796            )
797            .is_some()
798        {
799            warn!("compilation section {name} started more than once");
800        }
801    }
802
803    fn end_section(&mut self, name: &str, now: f64) {
804        let Some(section) = self.sections.get_mut(name) else {
805            warn!("compilation section {name} ended, but it has no start recorded");
806            return;
807        };
808        section.end = Some(now - self.start);
809    }
810}
811
812/// Start or end of a section timing.
813#[derive(serde::Deserialize, Debug)]
814#[serde(rename_all = "kebab-case")]
815pub enum SectionTimingEvent {
816    Start,
817    End,
818}
819
820/// Represents a certain section (phase) of rustc compilation.
821/// It is emitted by rustc when the `--json=timings` flag is used.
822#[derive(serde::Deserialize, Debug)]
823pub struct SectionTiming {
824    pub name: String,
825    pub event: SectionTimingEvent,
826}
827
828fn render_rustc_info(bcx: &BuildContext<'_, '_>) -> String {
829    let version = bcx
830        .rustc()
831        .verbose_version
832        .lines()
833        .next()
834        .expect("rustc version");
835    let requested_target = bcx
836        .build_config
837        .requested_kinds
838        .iter()
839        .map(|kind| bcx.target_data.short_name(kind))
840        .collect::<Vec<_>>()
841        .join(", ");
842    format!(
843        "{}<br>Host: {}<br>Target: {}",
844        version,
845        bcx.rustc().host,
846        requested_target
847    )
848}
849
850static HTML_TMPL: &str = r#"
851<html>
852<head>
853  <title>Cargo Build Timings — {ROOTS}</title>
854  <meta charset="utf-8">
855<style type="text/css">
856:root {
857  --error-text: #e80000;
858  --text: #000;
859  --background: #fff;
860  --h1-border-bottom: #c0c0c0;
861  --table-box-shadow: rgba(0, 0, 0, 0.1);
862  --table-th: #d5dde5;
863  --table-th-background: #1b1e24;
864  --table-th-border-bottom: #9ea7af;
865  --table-th-border-right: #343a45;
866  --table-tr-border-top: #c1c3d1;
867  --table-tr-border-bottom: #c1c3d1;
868  --table-tr-odd-background: #ebebeb;
869  --table-td-background: #ffffff;
870  --table-td-border-right: #C1C3D1;
871  --canvas-background: #f7f7f7;
872  --canvas-axes: #303030;
873  --canvas-grid: #e6e6e6;
874  --canvas-block: #aa95e8;
875  --canvas-custom-build: #f0b165;
876  --canvas-not-custom-build: #95cce8;
877  --canvas-dep-line: #ddd;
878  --canvas-dep-line-highlighted: #000;
879  --canvas-cpu: rgba(250, 119, 0, 0.2);
880}
881
882@media (prefers-color-scheme: dark) {
883  :root {
884    --error-text: #e80000;
885    --text: #fff;
886    --background: #121212;
887    --h1-border-bottom: #444;
888    --table-box-shadow: rgba(255, 255, 255, 0.1);
889    --table-th: #a0a0a0;
890    --table-th-background: #2c2c2c;
891    --table-th-border-bottom: #555;
892    --table-th-border-right: #444;
893    --table-tr-border-top: #333;
894    --table-tr-border-bottom: #333;
895    --table-tr-odd-background: #1e1e1e;
896    --table-td-background: #262626;
897    --table-td-border-right: #333;
898    --canvas-background: #1a1a1a;
899    --canvas-axes: #b0b0b0;
900    --canvas-grid: #333;
901    --canvas-block: #aa95e8;
902    --canvas-custom-build: #f0b165;
903    --canvas-not-custom-build: #95cce8;
904    --canvas-dep-line: #444;
905    --canvas-dep-line-highlighted: #fff;
906    --canvas-cpu: rgba(250, 119, 0, 0.2);
907  }
908}
909
910html {
911  font-family: sans-serif;
912  color: var(--text);
913  background: var(--background);
914}
915
916.canvas-container {
917  position: relative;
918  margin-top: 5px;
919  margin-bottom: 5px;
920}
921
922h1 {
923  border-bottom: 1px solid var(--h1-border-bottom);
924}
925
926.graph {
927  display: block;
928}
929
930.my-table {
931  margin-top: 20px;
932  margin-bottom: 20px;
933  border-collapse: collapse;
934  box-shadow: 0 5px 10px var(--table-box-shadow);
935}
936
937.my-table th {
938  color: var(--table-th);
939  background: var(--table-th-background);
940  border-bottom: 4px solid var(--table-th-border-bottom);
941  border-right: 1px solid var(--table-th-border-right);
942  font-size: 18px;
943  font-weight: 100;
944  padding: 12px;
945  text-align: left;
946  vertical-align: middle;
947}
948
949.my-table th:first-child {
950  border-top-left-radius: 3px;
951}
952
953.my-table th:last-child {
954  border-top-right-radius: 3px;
955  border-right:none;
956}
957
958.my-table tr {
959  border-top: 1px solid var(--table-tr-border-top);
960  border-bottom: 1px solid var(--table-tr-border-bottom);
961  font-size: 16px;
962  font-weight: normal;
963}
964
965.my-table tr:first-child {
966  border-top:none;
967}
968
969.my-table tr:last-child {
970  border-bottom:none;
971}
972
973.my-table tr:nth-child(odd) td {
974  background: var(--table-tr-odd-background);
975}
976
977.my-table tr:last-child td:first-child {
978  border-bottom-left-radius:3px;
979}
980
981.my-table tr:last-child td:last-child {
982  border-bottom-right-radius:3px;
983}
984
985.my-table td {
986  background: var(--table-td-background);
987  padding: 10px;
988  text-align: left;
989  vertical-align: middle;
990  font-weight: 300;
991  font-size: 14px;
992  border-right: 1px solid var(--table-td-border-right);
993}
994
995.my-table td:last-child {
996  border-right: 0px;
997}
998
999.summary-table td:first-child {
1000  vertical-align: top;
1001  text-align: right;
1002}
1003
1004.input-table td {
1005  text-align: center;
1006}
1007
1008.error-text {
1009  color: var(--error-text);
1010}
1011
1012</style>
1013</head>
1014<body>
1015
1016<h1>Cargo Build Timings</h1>
1017See <a href="https://doc.rust-lang.org/nightly/cargo/reference/timings.html">Documentation</a>
1018"#;
1019
1020static HTML_CANVAS: &str = r#"
1021<table class="input-table">
1022  <tr>
1023    <td><label for="min-unit-time">Min unit time:</label></td>
1024    <td title="Scale corresponds to a number of pixels per second. It is automatically initialized based on your viewport width.">
1025      <label for="scale">Scale:</label>
1026    </td>
1027  </tr>
1028  <tr>
1029    <td><input type="range" min="0" max="30" step="0.1" value="0" id="min-unit-time"></td>
1030    <!--
1031        The scale corresponds to some number of "pixels per second".
1032        Its min, max, and initial values are automatically set by JavaScript on page load,
1033        based on the client viewport.
1034    -->
1035    <td><input type="range" min="1" max="100" value="50" id="scale"></td>
1036  </tr>
1037  <tr>
1038    <td><output for="min-unit-time" id="min-unit-time-output"></output></td>
1039    <td><output for="scale" id="scale-output"></output></td>
1040  </tr>
1041</table>
1042
1043<div id="pipeline-container" class="canvas-container">
1044 <canvas id="pipeline-graph" class="graph" style="position: absolute; left: 0; top: 0; z-index: 0;"></canvas>
1045 <canvas id="pipeline-graph-lines" style="position: absolute; left: 0; top: 0; z-index: 1; pointer-events:none;"></canvas>
1046</div>
1047<div class="canvas-container">
1048  <canvas id="timing-graph" class="graph"></canvas>
1049</div>
1050"#;