Skip to content

Commit 78e1edd

Browse files
committed
Add section timing data to JSON --timings output
1 parent 5a266b2 commit 78e1edd

File tree

1 file changed

+189
-20
lines changed

1 file changed

+189
-20
lines changed

src/cargo/core/compiler/timings.rs

Lines changed: 189 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use crate::util::{CargoResult, GlobalContext};
1313
use anyhow::Context as _;
1414
use cargo_util::paths;
1515
use indexmap::IndexMap;
16+
use itertools::Itertools;
1617
use std::collections::HashMap;
1718
use std::io::{BufWriter, Write};
1819
use std::thread::available_parallelism;
@@ -90,11 +91,75 @@ struct UnitTime {
9091
unlocked_units: Vec<Unit>,
9192
/// Same as `unlocked_units`, but unlocked by rmeta.
9293
unlocked_rmeta_units: Vec<Unit>,
94+
/// Individual compilation section durations, gathered from `--json=timings`.
9395
/// IndexMap is used to keep original insertion order, we want to be able to tell which
9496
/// sections were started in which order.
9597
sections: IndexMap<String, CompilationSection>,
9698
}
9799

100+
const FRONTEND_SECTION_NAME: &str = "Frontend";
101+
const CODEGEN_SECTION_NAME: &str = "Codegen";
102+
103+
impl UnitTime {
104+
fn aggregate_sections(&self) -> AggregatedSections {
105+
let end = self.duration;
106+
107+
if !self.sections.is_empty() {
108+
// We have some detailed compilation section timings, so we postprocess them
109+
// Since it is possible that we do not have an end timestamp for a given compilation
110+
// section, we need to iterate them and if an end is missing, we assign the end of
111+
// the section to the start of the following section.
112+
113+
let mut sections = vec![];
114+
115+
// The frontend section is currently implicit in rustc, it is assumed to start at
116+
// compilation start and end when codegen starts. So we hard-code it here.
117+
let mut previous_section = (
118+
FRONTEND_SECTION_NAME.to_string(),
119+
CompilationSection {
120+
start: 0.0,
121+
end: None,
122+
},
123+
);
124+
for (name, section) in self.sections.clone() {
125+
// Store the previous section, potentially setting its end to the start of the
126+
// current section.
127+
sections.push((
128+
previous_section.0.clone(),
129+
SectionData {
130+
start: previous_section.1.start,
131+
end: previous_section.1.end.unwrap_or(section.start),
132+
},
133+
));
134+
previous_section = (name, section);
135+
}
136+
// Store the last section, potentially setting its end to the end of the whole
137+
// compilation.
138+
sections.push((
139+
previous_section.0.clone(),
140+
SectionData {
141+
start: previous_section.1.start,
142+
end: previous_section.1.end.unwrap_or(end),
143+
},
144+
));
145+
146+
AggregatedSections::Sections(sections)
147+
} else if let Some(rmeta) = self.rmeta_time {
148+
// We only know when the rmeta time was generated
149+
AggregatedSections::OnlyMetadataTime {
150+
frontend: SectionData {
151+
start: 0.0,
152+
end: rmeta,
153+
},
154+
codegen: SectionData { start: rmeta, end },
155+
}
156+
} else {
157+
// We only know the total duration
158+
AggregatedSections::OnlyTotalDuration
159+
}
160+
}
161+
}
162+
98163
/// Periodic concurrency tracking information.
99164
#[derive(serde::Serialize)]
100165
struct Concurrency {
@@ -109,6 +174,35 @@ struct Concurrency {
109174
inactive: usize,
110175
}
111176

177+
/// Postprocessed section data that has both start and an end.
178+
#[derive(Copy, Clone, serde::Serialize)]
179+
struct SectionData {
180+
/// Start (relative to the start of the unit)
181+
start: f64,
182+
/// End (relative to the start of the unit)
183+
end: f64,
184+
}
185+
186+
impl SectionData {
187+
fn duration(&self) -> f64 {
188+
(self.end - self.start).max(0.0)
189+
}
190+
}
191+
192+
/// Contains post-processed data of individual compilation sections.
193+
#[derive(serde::Serialize)]
194+
enum 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+
112206
impl<'gctx> Timings<'gctx> {
113207
pub fn new(bcx: &BuildContext<'_, 'gctx>, root_units: &[Unit]) -> Timings<'gctx> {
114208
let has_report = |what| bcx.build_config.timing_outputs.contains(&what);
@@ -480,6 +574,7 @@ impl<'gctx> Timings<'gctx> {
480574
.enumerate()
481575
.map(|(i, ut)| (ut.unit.clone(), i))
482576
.collect();
577+
483578
#[derive(serde::Serialize)]
484579
struct UnitData {
485580
i: usize,
@@ -553,6 +648,58 @@ impl<'gctx> Timings<'gctx> {
553648

554649
/// Render the table of all units.
555650
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+
556703
write!(
557704
f,
558705
r#"
@@ -562,20 +709,53 @@ impl<'gctx> Timings<'gctx> {
562709
<th></th>
563710
<th>Unit</th>
564711
<th>Total</th>
565-
<th>Codegen</th>
712+
{headers}
566713
<th>Features</th>
567714
</tr>
568715
</thead>
569716
<tbody>
570-
"#
717+
"#,
718+
headers = headers.iter().map(|h| format!("<th>{h}</th>")).join("\n")
571719
)?;
572-
let mut units: Vec<&UnitTime> = self.unit_times.iter().collect();
573-
units.sort_unstable_by(|a, b| b.duration.partial_cmp(&a.duration).unwrap());
574-
for (i, unit) in units.iter().enumerate() {
575-
let codegen = match unit.codegen_time() {
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+
}
576728
None => "".to_string(),
577-
Some((_rt, ctime, cent)) => format!("{:.1}s ({:.0}%)", ctime, cent),
578729
};
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+
579759
let features = unit.unit.features.join(", ");
580760
write!(
581761
f,
@@ -584,16 +764,14 @@ impl<'gctx> Timings<'gctx> {
584764
<td>{}.</td>
585765
<td>{}{}</td>
586766
<td>{:.1}s</td>
587-
<td>{}</td>
588-
<td>{}</td>
767+
{cells}
768+
<td>{features}</td>
589769
</tr>
590770
"#,
591771
i + 1,
592772
unit.name_ver(),
593773
unit.target,
594774
unit.duration,
595-
codegen,
596-
features,
597775
)?;
598776
}
599777
write!(f, "</tbody>\n</table>\n")?;
@@ -602,15 +780,6 @@ impl<'gctx> Timings<'gctx> {
602780
}
603781

604782
impl UnitTime {
605-
/// Returns the codegen time as (`rmeta_time`, `codegen_time`, percent of total)
606-
fn codegen_time(&self) -> Option<(f64, f64, f64)> {
607-
self.rmeta_time.map(|rmeta_time| {
608-
let ctime = self.duration - rmeta_time;
609-
let cent = (ctime / self.duration) * 100.0;
610-
(rmeta_time, ctime, cent)
611-
})
612-
}
613-
614783
fn name_ver(&self) -> String {
615784
format!("{} v{}", self.unit.pkg.name(), self.unit.pkg.version())
616785
}

0 commit comments

Comments
 (0)