]>
Commit | Line | Data |
---|---|---|
923072b8 FG |
1 | //! This module is responsible for collecting metrics profiling information for the current build |
2 | //! and dumping it to disk as JSON, to aid investigations on build and CI performance. | |
3 | //! | |
4 | //! As this module requires additional dependencies not present during local builds, it's cfg'd | |
5 | //! away whenever the `build.metrics` config option is not set to `true`. | |
6 | ||
49aad941 | 7 | use crate::builder::{Builder, Step}; |
923072b8 FG |
8 | use crate::util::t; |
9 | use crate::Build; | |
add651ee FG |
10 | use build_helper::metrics::{ |
11 | JsonInvocation, JsonInvocationSystemStats, JsonNode, JsonRoot, JsonStepSystemStats, Test, | |
12 | TestOutcome, TestSuite, TestSuiteMetadata, | |
13 | }; | |
923072b8 FG |
14 | use std::cell::RefCell; |
15 | use std::fs::File; | |
16 | use std::io::BufWriter; | |
353b0b11 | 17 | use std::time::{Duration, Instant, SystemTime}; |
923072b8 FG |
18 | use sysinfo::{CpuExt, System, SystemExt}; |
19 | ||
49aad941 FG |
20 | // Update this number whenever a breaking change is made to the build metrics. |
21 | // | |
22 | // The output format is versioned for two reasons: | |
23 | // | |
24 | // - The metadata is intended to be consumed by external tooling, and exposing a format version | |
25 | // helps the tools determine whether they're compatible with a metrics file. | |
26 | // | |
27 | // - If a developer enables build metrics in their local checkout, making a breaking change to the | |
28 | // metrics format would result in a hard-to-diagnose error message when an existing metrics file | |
29 | // is not compatible with the new changes. With a format version number, bootstrap can discard | |
30 | // incompatible metrics files instead of appending metrics to them. | |
31 | // | |
32 | // Version changelog: | |
33 | // | |
34 | // - v0: initial version | |
35 | // - v1: replaced JsonNode::Test with JsonNode::TestSuite | |
36 | // | |
37 | const CURRENT_FORMAT_VERSION: usize = 1; | |
38 | ||
923072b8 FG |
39 | pub(crate) struct BuildMetrics { |
40 | state: RefCell<MetricsState>, | |
41 | } | |
42 | ||
add651ee FG |
43 | /// NOTE: this isn't really cloning anything, but `x suggest` doesn't need metrics so this is probably ok. |
44 | impl Clone for BuildMetrics { | |
45 | fn clone(&self) -> Self { | |
46 | Self::init() | |
47 | } | |
48 | } | |
49 | ||
923072b8 FG |
50 | impl BuildMetrics { |
51 | pub(crate) fn init() -> Self { | |
52 | let state = RefCell::new(MetricsState { | |
53 | finished_steps: Vec::new(), | |
54 | running_steps: Vec::new(), | |
55 | ||
56 | system_info: System::new(), | |
57 | timer_start: None, | |
58 | invocation_timer_start: Instant::now(), | |
353b0b11 | 59 | invocation_start: SystemTime::now(), |
923072b8 FG |
60 | }); |
61 | ||
62 | BuildMetrics { state } | |
63 | } | |
64 | ||
49aad941 FG |
65 | pub(crate) fn enter_step<S: Step>(&self, step: &S, builder: &Builder<'_>) { |
66 | // Do not record dry runs, as they'd be duplicates of the actual steps. | |
67 | if builder.config.dry_run() { | |
68 | return; | |
69 | } | |
70 | ||
923072b8 FG |
71 | let mut state = self.state.borrow_mut(); |
72 | ||
73 | // Consider all the stats gathered so far as the parent's. | |
74 | if !state.running_steps.is_empty() { | |
75 | self.collect_stats(&mut *state); | |
76 | } | |
77 | ||
78 | state.system_info.refresh_cpu(); | |
79 | state.timer_start = Some(Instant::now()); | |
80 | ||
81 | state.running_steps.push(StepMetrics { | |
82 | type_: std::any::type_name::<S>().into(), | |
83 | debug_repr: format!("{step:?}"), | |
84 | ||
85 | cpu_usage_time_sec: 0.0, | |
86 | duration_excluding_children_sec: Duration::ZERO, | |
87 | ||
88 | children: Vec::new(), | |
49aad941 | 89 | test_suites: Vec::new(), |
923072b8 FG |
90 | }); |
91 | } | |
92 | ||
49aad941 FG |
93 | pub(crate) fn exit_step(&self, builder: &Builder<'_>) { |
94 | // Do not record dry runs, as they'd be duplicates of the actual steps. | |
95 | if builder.config.dry_run() { | |
96 | return; | |
97 | } | |
98 | ||
923072b8 FG |
99 | let mut state = self.state.borrow_mut(); |
100 | ||
101 | self.collect_stats(&mut *state); | |
102 | ||
103 | let step = state.running_steps.pop().unwrap(); | |
104 | if state.running_steps.is_empty() { | |
105 | state.finished_steps.push(step); | |
106 | state.timer_start = None; | |
107 | } else { | |
108 | state.running_steps.last_mut().unwrap().children.push(step); | |
109 | ||
110 | // Start collecting again for the parent step. | |
111 | state.system_info.refresh_cpu(); | |
112 | state.timer_start = Some(Instant::now()); | |
113 | } | |
114 | } | |
115 | ||
49aad941 FG |
116 | pub(crate) fn begin_test_suite(&self, metadata: TestSuiteMetadata, builder: &Builder<'_>) { |
117 | // Do not record dry runs, as they'd be duplicates of the actual steps. | |
118 | if builder.config.dry_run() { | |
119 | return; | |
120 | } | |
121 | ||
122 | let mut state = self.state.borrow_mut(); | |
123 | let step = state.running_steps.last_mut().unwrap(); | |
124 | step.test_suites.push(TestSuite { metadata, tests: Vec::new() }); | |
125 | } | |
126 | ||
127 | pub(crate) fn record_test(&self, name: &str, outcome: TestOutcome, builder: &Builder<'_>) { | |
128 | // Do not record dry runs, as they'd be duplicates of the actual steps. | |
129 | if builder.config.dry_run() { | |
130 | return; | |
131 | } | |
132 | ||
353b0b11 | 133 | let mut state = self.state.borrow_mut(); |
49aad941 FG |
134 | let step = state.running_steps.last_mut().unwrap(); |
135 | ||
136 | if let Some(test_suite) = step.test_suites.last_mut() { | |
137 | test_suite.tests.push(Test { name: name.to_string(), outcome }); | |
138 | } else { | |
139 | panic!("metrics.record_test() called without calling metrics.begin_test_suite() first"); | |
140 | } | |
353b0b11 FG |
141 | } |
142 | ||
923072b8 FG |
143 | fn collect_stats(&self, state: &mut MetricsState) { |
144 | let step = state.running_steps.last_mut().unwrap(); | |
145 | ||
146 | let elapsed = state.timer_start.unwrap().elapsed(); | |
147 | step.duration_excluding_children_sec += elapsed; | |
148 | ||
149 | state.system_info.refresh_cpu(); | |
150 | let cpu = state.system_info.cpus().iter().map(|p| p.cpu_usage()).sum::<f32>(); | |
151 | step.cpu_usage_time_sec += cpu as f64 / 100.0 * elapsed.as_secs_f64(); | |
152 | } | |
153 | ||
154 | pub(crate) fn persist(&self, build: &Build) { | |
155 | let mut state = self.state.borrow_mut(); | |
156 | assert!(state.running_steps.is_empty(), "steps are still executing"); | |
157 | ||
158 | let dest = build.out.join("metrics.json"); | |
159 | ||
160 | let mut system = System::new(); | |
161 | system.refresh_cpu(); | |
162 | system.refresh_memory(); | |
163 | ||
164 | let system_stats = JsonInvocationSystemStats { | |
165 | cpu_threads_count: system.cpus().len(), | |
166 | cpu_model: system.cpus()[0].brand().into(), | |
167 | ||
487cf647 | 168 | memory_total_bytes: system.total_memory(), |
923072b8 FG |
169 | }; |
170 | let steps = std::mem::take(&mut state.finished_steps); | |
171 | ||
172 | // Some of our CI builds consist of multiple independent CI invocations. Ensure all the | |
173 | // previous invocations are still present in the resulting file. | |
174 | let mut invocations = match std::fs::read(&dest) { | |
49aad941 FG |
175 | Ok(contents) => { |
176 | // We first parse just the format_version field to have the check succeed even if | |
177 | // the rest of the contents are not valid anymore. | |
178 | let version: OnlyFormatVersion = t!(serde_json::from_slice(&contents)); | |
179 | if version.format_version == CURRENT_FORMAT_VERSION { | |
180 | t!(serde_json::from_slice::<JsonRoot>(&contents)).invocations | |
181 | } else { | |
182 | println!( | |
183 | "warning: overriding existing build/metrics.json, as it's not \ | |
184 | compatible with build metrics format version {CURRENT_FORMAT_VERSION}." | |
185 | ); | |
186 | Vec::new() | |
187 | } | |
188 | } | |
923072b8 FG |
189 | Err(err) => { |
190 | if err.kind() != std::io::ErrorKind::NotFound { | |
191 | panic!("failed to open existing metrics file at {}: {err}", dest.display()); | |
192 | } | |
193 | Vec::new() | |
194 | } | |
195 | }; | |
196 | invocations.push(JsonInvocation { | |
353b0b11 FG |
197 | start_time: state |
198 | .invocation_start | |
199 | .duration_since(SystemTime::UNIX_EPOCH) | |
200 | .unwrap() | |
201 | .as_secs(), | |
923072b8 FG |
202 | duration_including_children_sec: state.invocation_timer_start.elapsed().as_secs_f64(), |
203 | children: steps.into_iter().map(|step| self.prepare_json_step(step)).collect(), | |
204 | }); | |
205 | ||
49aad941 | 206 | let json = JsonRoot { format_version: CURRENT_FORMAT_VERSION, system_stats, invocations }; |
923072b8 FG |
207 | |
208 | t!(std::fs::create_dir_all(dest.parent().unwrap())); | |
209 | let mut file = BufWriter::new(t!(File::create(&dest))); | |
210 | t!(serde_json::to_writer(&mut file, &json)); | |
211 | } | |
212 | ||
213 | fn prepare_json_step(&self, step: StepMetrics) -> JsonNode { | |
353b0b11 FG |
214 | let mut children = Vec::new(); |
215 | children.extend(step.children.into_iter().map(|child| self.prepare_json_step(child))); | |
49aad941 | 216 | children.extend(step.test_suites.into_iter().map(JsonNode::TestSuite)); |
353b0b11 | 217 | |
923072b8 FG |
218 | JsonNode::RustbuildStep { |
219 | type_: step.type_, | |
220 | debug_repr: step.debug_repr, | |
221 | ||
222 | duration_excluding_children_sec: step.duration_excluding_children_sec.as_secs_f64(), | |
223 | system_stats: JsonStepSystemStats { | |
224 | cpu_utilization_percent: step.cpu_usage_time_sec * 100.0 | |
225 | / step.duration_excluding_children_sec.as_secs_f64(), | |
226 | }, | |
227 | ||
353b0b11 | 228 | children, |
923072b8 FG |
229 | } |
230 | } | |
231 | } | |
232 | ||
233 | struct MetricsState { | |
234 | finished_steps: Vec<StepMetrics>, | |
235 | running_steps: Vec<StepMetrics>, | |
236 | ||
237 | system_info: System, | |
238 | timer_start: Option<Instant>, | |
239 | invocation_timer_start: Instant, | |
353b0b11 | 240 | invocation_start: SystemTime, |
923072b8 FG |
241 | } |
242 | ||
243 | struct StepMetrics { | |
244 | type_: String, | |
245 | debug_repr: String, | |
246 | ||
247 | cpu_usage_time_sec: f64, | |
248 | duration_excluding_children_sec: Duration, | |
249 | ||
250 | children: Vec<StepMetrics>, | |
49aad941 | 251 | test_suites: Vec<TestSuite>, |
923072b8 FG |
252 | } |
253 | ||
add651ee | 254 | #[derive(serde_derive::Deserialize)] |
49aad941 FG |
255 | struct OnlyFormatVersion { |
256 | #[serde(default)] // For version 0 the field was not present. | |
257 | format_version: usize, | |
258 | } |