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)] 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#[derive(Serialize, Deserialize)]
18pub struct CiMetadata {
19 pub workflow_run_id: u64,
22 pub repository: String,
25}
26
27#[derive(Serialize, Deserialize)]
28#[serde(rename_all = "snake_case")]
29pub struct JsonInvocation {
30 pub cmdline: String,
32 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#[derive(Clone, Debug)]
121pub struct BuildStep {
122 pub r#type: String,
123 pub children: Vec<BuildStep>,
124 pub duration: Duration,
125 pub full_name: String,
127}
128
129impl BuildStep {
130 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 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 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
200pub 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
212pub fn escape_step_name(step: &BuildStep) -> String {
215 step.r#type.replace('<', "<").replace('>', ">")
216}