build_helper/
metrics.rs

1use std::time::Duration;
2
3use serde_derive::{Deserialize, Serialize};
4
5#[derive(Serialize, Deserialize)]
6#[serde(rename_all = "snake_case")]
7pub struct JsonRoot {
8    #[serde(default)] // For version 0 the field was not present.
9    pub format_version: usize,
10    pub system_stats: JsonInvocationSystemStats,
11    pub invocations: Vec<JsonInvocation>,
12    #[serde(default)]
13    pub ci_metadata: Option<CiMetadata>,
14}
15
16/// Represents metadata about bootstrap's execution in CI.
17#[derive(Serialize, Deserialize)]
18pub struct CiMetadata {
19    /// GitHub run ID of the workflow where bootstrap was executed.
20    /// Note that the run ID will be shared amongst all jobs executed in that workflow.
21    pub workflow_run_id: u64,
22    /// Full name of a GitHub repository where bootstrap was executed in CI.
23    /// e.g. `rust-lang-ci/rust`.
24    pub repository: String,
25}
26
27#[derive(Serialize, Deserialize)]
28#[serde(rename_all = "snake_case")]
29pub struct JsonInvocation {
30    // Remembers the command-line invocation with which bootstrap was invoked.
31    pub cmdline: String,
32    // Unix timestamp in seconds
33    //
34    // This is necessary to easily correlate this invocation with logs or other data.
35    pub start_time: u64,
36    #[serde(deserialize_with = "null_as_f64_nan")]
37    pub duration_including_children_sec: f64,
38    pub children: Vec<JsonNode>,
39}
40
41#[derive(Serialize, Deserialize)]
42#[serde(tag = "kind", rename_all = "snake_case")]
43pub enum JsonNode {
44    RustbuildStep {
45        #[serde(rename = "type")]
46        type_: String,
47        debug_repr: String,
48
49        #[serde(deserialize_with = "null_as_f64_nan")]
50        duration_excluding_children_sec: f64,
51        system_stats: JsonStepSystemStats,
52
53        children: Vec<JsonNode>,
54    },
55    TestSuite(TestSuite),
56}
57
58#[derive(Serialize, Deserialize)]
59pub struct TestSuite {
60    pub metadata: TestSuiteMetadata,
61    pub tests: Vec<Test>,
62}
63
64#[derive(Serialize, Deserialize)]
65#[serde(tag = "kind", rename_all = "snake_case")]
66pub enum TestSuiteMetadata {
67    CargoPackage {
68        crates: Vec<String>,
69        target: String,
70        host: String,
71        stage: u32,
72    },
73    Compiletest {
74        suite: String,
75        mode: String,
76        compare_mode: Option<String>,
77        target: String,
78        host: String,
79        stage: u32,
80    },
81}
82
83#[derive(Serialize, Deserialize)]
84pub struct Test {
85    pub name: String,
86    #[serde(flatten)]
87    pub outcome: TestOutcome,
88}
89
90#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
91#[serde(tag = "outcome", rename_all = "snake_case")]
92pub enum TestOutcome {
93    Passed,
94    Failed,
95    Ignored { ignore_reason: Option<String> },
96}
97
98#[derive(Serialize, Deserialize)]
99#[serde(rename_all = "snake_case")]
100pub struct JsonInvocationSystemStats {
101    pub cpu_threads_count: usize,
102    pub cpu_model: String,
103
104    pub memory_total_bytes: u64,
105}
106
107#[derive(Serialize, Deserialize)]
108#[serde(rename_all = "snake_case")]
109pub struct JsonStepSystemStats {
110    #[serde(deserialize_with = "null_as_f64_nan")]
111    pub cpu_utilization_percent: f64,
112}
113
114fn null_as_f64_nan<'de, D: serde::Deserializer<'de>>(d: D) -> Result<f64, D::Error> {
115    use serde::Deserialize as _;
116    Option::<f64>::deserialize(d).map(|f| f.unwrap_or(f64::NAN))
117}
118
119/// Represents a single bootstrap step, with the accumulated duration of all its children.
120#[derive(Clone, Debug)]
121pub struct BuildStep {
122    pub r#type: String,
123    pub children: Vec<BuildStep>,
124    pub duration: Duration,
125    // Full name of the step, including all parent names
126    pub full_name: String,
127}
128
129impl BuildStep {
130    /// Create a `BuildStep` representing a single invocation of bootstrap.
131    /// The most important thing is that the build step aggregates the
132    /// durations of all children, so that it can be easily accessed.
133    pub fn from_invocation(invocation: &JsonInvocation) -> Self {
134        fn parse(node: &JsonNode, parent_name: &str) -> Option<BuildStep> {
135            match node {
136                JsonNode::RustbuildStep {
137                    type_: kind,
138                    children,
139                    duration_excluding_children_sec,
140                    ..
141                } => {
142                    let full_name = format!("{parent_name}-{kind}");
143                    let children: Vec<_> =
144                        children.into_iter().filter_map(|s| parse(s, &full_name)).collect();
145                    let children_duration = children.iter().map(|c| c.duration).sum::<Duration>();
146                    Some(BuildStep {
147                        r#type: kind.to_string(),
148                        children,
149                        full_name,
150                        duration: children_duration
151                            + Duration::from_secs_f64(*duration_excluding_children_sec),
152                    })
153                }
154                JsonNode::TestSuite(_) => None,
155            }
156        }
157
158        let duration = Duration::from_secs_f64(invocation.duration_including_children_sec);
159
160        // The root "total" step is kind of a virtual step that encompasses all other steps,
161        // but it is not a real parent of the other steps.
162        // We thus start the parent name hierarchy here and use an empty string
163        // as the parent name of the top-level steps.
164        let children: Vec<_> = invocation.children.iter().filter_map(|s| parse(s, "")).collect();
165        Self { r#type: "total".to_string(), children, duration, full_name: "total".to_string() }
166    }
167
168    pub fn find_all_by_type(&self, r#type: &str) -> Vec<&Self> {
169        let mut result = Vec::new();
170        self.find_by_type(r#type, &mut result);
171        result
172    }
173
174    fn find_by_type<'a>(&'a self, r#type: &str, result: &mut Vec<&'a Self>) {
175        if self.r#type == r#type {
176            result.push(self);
177        }
178        for child in &self.children {
179            child.find_by_type(r#type, result);
180        }
181    }
182
183    /// Returns a Vec with all substeps, ordered by their hierarchical order.
184    /// The first element of the tuple is the depth of a given step.
185    pub fn linearize_steps(&self) -> Vec<(u32, &BuildStep)> {
186        let mut substeps: Vec<(u32, &BuildStep)> = Vec::new();
187
188        fn visit<'a>(step: &'a BuildStep, level: u32, substeps: &mut Vec<(u32, &'a BuildStep)>) {
189            substeps.push((level, step));
190            for child in &step.children {
191                visit(child, level + 1, substeps);
192            }
193        }
194
195        visit(self, 0, &mut substeps);
196        substeps
197    }
198}
199
200/// Writes build steps into a nice indented table.
201pub fn format_build_steps(root: &BuildStep) -> String {
202    use std::fmt::Write;
203
204    let mut output = String::new();
205    for (level, step) in root.linearize_steps() {
206        let label = format!("{}{}", ".".repeat(level as usize), escape_step_name(step));
207        writeln!(output, "{label:.<65}{:>8.2}s", step.duration.as_secs_f64()).unwrap();
208    }
209    output
210}
211
212/// Bootstrap steps can be generic and thus contain angle brackets (<...>).
213/// However, Markdown interprets these as HTML, so we need to escap ethem.
214pub fn escape_step_name(step: &BuildStep) -> String {
215    step.r#type.replace('<', "&lt;").replace('>', "&gt;")
216}