Skip to content

Commit 53965d3

Browse files
committed
add build metrics, to gather ci stats from x.py
This tool will generate a JSON file with statistics about each individual step to disk. It will be used in rust-lang/rust's CI to replace the mix of scripts and log scraping we currently have to gather this data.
1 parent f75d884 commit 53965d3

File tree

8 files changed

+258
-0
lines changed

8 files changed

+258
-0
lines changed

Cargo.lock

+16
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ dependencies = [
223223
"pretty_assertions",
224224
"serde",
225225
"serde_json",
226+
"sysinfo",
226227
"tar",
227228
"toml",
228229
"winapi",
@@ -5057,6 +5058,21 @@ dependencies = [
50575058
"unicode-xid",
50585059
]
50595060

5061+
[[package]]
5062+
name = "sysinfo"
5063+
version = "0.23.11"
5064+
source = "registry+https://github.com/rust-lang/crates.io-index"
5065+
checksum = "3bf915673a340ee41f2fc24ad1286c75ea92026f04b65a0d0e5132d80b95fc61"
5066+
dependencies = [
5067+
"cfg-if 1.0.0",
5068+
"core-foundation-sys",
5069+
"libc",
5070+
"ntapi",
5071+
"once_cell",
5072+
"rayon",
5073+
"winapi",
5074+
]
5075+
50605076
[[package]]
50615077
name = "tar"
50625078
version = "0.4.37"

config.toml.example

+6
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,12 @@ changelog-seen = 2
328328
# a Nix toolchain on non-NixOS distributions.
329329
#patch-binaries-for-nix = false
330330

331+
# Collect information and statistics about the current build and writes it to
332+
# disk. Enabling this or not has no impact on the resulting build output. The
333+
# schema of the file generated by the build metrics feature is unstable, and
334+
# this is not intended to be used during local development.
335+
#metrics = false
336+
331337
# =============================================================================
332338
# General install configuration options
333339
# =============================================================================

src/bootstrap/Cargo.toml

+6
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ opener = "0.5"
4949
once_cell = "1.7.2"
5050
xz2 = "0.1"
5151

52+
# Dependencies needed by the build-metrics feature
53+
sysinfo = { version = "0.23.0", optional = true }
54+
5255
[target.'cfg(windows)'.dependencies.winapi]
5356
version = "0.3"
5457
features = [
@@ -64,3 +67,6 @@ features = [
6467

6568
[dev-dependencies]
6669
pretty_assertions = "0.7"
70+
71+
[features]
72+
build-metrics = ["sysinfo"]

src/bootstrap/bootstrap.py

+3
Original file line numberDiff line numberDiff line change
@@ -896,6 +896,9 @@ def build_bootstrap(self):
896896
args.append("--locked")
897897
if self.use_vendored_sources:
898898
args.append("--frozen")
899+
if self.get_toml("metrics", "build"):
900+
args.append("--features")
901+
args.append("build-metrics")
899902
run(args, env=env, verbose=self.verbose)
900903

901904
def build_triple(self):

src/bootstrap/builder.rs

+6
Original file line numberDiff line numberDiff line change
@@ -1757,6 +1757,9 @@ impl<'a> Builder<'a> {
17571757
stack.push(Box::new(step.clone()));
17581758
}
17591759

1760+
#[cfg(feature = "build-metrics")]
1761+
self.metrics.enter_step(&step);
1762+
17601763
let (out, dur) = {
17611764
let start = Instant::now();
17621765
let zero = Duration::new(0, 0);
@@ -1780,6 +1783,9 @@ impl<'a> Builder<'a> {
17801783
);
17811784
}
17821785

1786+
#[cfg(feature = "build-metrics")]
1787+
self.metrics.exit_step();
1788+
17831789
{
17841790
let mut stack = self.stack.borrow_mut();
17851791
let cur_step = stack.pop().expect("step stack empty");

src/bootstrap/config.rs

+1
Original file line numberDiff line numberDiff line change
@@ -544,6 +544,7 @@ define_config! {
544544
dist_stage: Option<u32> = "dist-stage",
545545
bench_stage: Option<u32> = "bench-stage",
546546
patch_binaries_for_nix: Option<bool> = "patch-binaries-for-nix",
547+
metrics: Option<bool> = "metrics",
547548
}
548549
}
549550

src/bootstrap/lib.rs

+12
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,9 @@ mod tool;
149149
mod toolstate;
150150
pub mod util;
151151

152+
#[cfg(feature = "build-metrics")]
153+
mod metrics;
154+
152155
#[cfg(windows)]
153156
mod job;
154157

@@ -311,6 +314,9 @@ pub struct Build {
311314
prerelease_version: Cell<Option<u32>>,
312315
tool_artifacts:
313316
RefCell<HashMap<TargetSelection, HashMap<String, (&'static str, PathBuf, Vec<String>)>>>,
317+
318+
#[cfg(feature = "build-metrics")]
319+
metrics: metrics::BuildMetrics,
314320
}
315321

316322
#[derive(Debug)]
@@ -500,6 +506,9 @@ impl Build {
500506
delayed_failures: RefCell::new(Vec::new()),
501507
prerelease_version: Cell::new(None),
502508
tool_artifacts: Default::default(),
509+
510+
#[cfg(feature = "build-metrics")]
511+
metrics: metrics::BuildMetrics::init(),
503512
};
504513

505514
build.verbose("finding compilers");
@@ -692,6 +701,9 @@ impl Build {
692701
}
693702
process::exit(1);
694703
}
704+
705+
#[cfg(feature = "build-metrics")]
706+
self.metrics.persist(self);
695707
}
696708

697709
/// Clear out `dir` if `input` is newer.

src/bootstrap/metrics.rs

+208
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
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+
7+
use crate::builder::Step;
8+
use crate::util::t;
9+
use crate::Build;
10+
use serde::{Deserialize, Serialize};
11+
use std::cell::RefCell;
12+
use std::fs::File;
13+
use std::io::BufWriter;
14+
use std::time::{Duration, Instant};
15+
use sysinfo::{ProcessorExt, System, SystemExt};
16+
17+
pub(crate) struct BuildMetrics {
18+
state: RefCell<MetricsState>,
19+
}
20+
21+
impl BuildMetrics {
22+
pub(crate) fn init() -> Self {
23+
let state = RefCell::new(MetricsState {
24+
finished_steps: Vec::new(),
25+
running_steps: Vec::new(),
26+
27+
system_info: System::new(),
28+
timer_start: None,
29+
invocation_timer_start: Instant::now(),
30+
});
31+
32+
BuildMetrics { state }
33+
}
34+
35+
pub(crate) fn enter_step<S: Step>(&self, step: &S) {
36+
let mut state = self.state.borrow_mut();
37+
38+
// Consider all the stats gathered so far as the parent's.
39+
if !state.running_steps.is_empty() {
40+
self.collect_stats(&mut *state);
41+
}
42+
43+
state.system_info.refresh_cpu();
44+
state.timer_start = Some(Instant::now());
45+
46+
state.running_steps.push(StepMetrics {
47+
type_: std::any::type_name::<S>().into(),
48+
debug_repr: format!("{step:?}"),
49+
50+
cpu_usage_time_sec: 0.0,
51+
duration_excluding_children_sec: Duration::ZERO,
52+
53+
children: Vec::new(),
54+
});
55+
}
56+
57+
pub(crate) fn exit_step(&self) {
58+
let mut state = self.state.borrow_mut();
59+
60+
self.collect_stats(&mut *state);
61+
62+
let step = state.running_steps.pop().unwrap();
63+
if state.running_steps.is_empty() {
64+
state.finished_steps.push(step);
65+
state.timer_start = None;
66+
} else {
67+
state.running_steps.last_mut().unwrap().children.push(step);
68+
69+
// Start collecting again for the parent step.
70+
state.system_info.refresh_cpu();
71+
state.timer_start = Some(Instant::now());
72+
}
73+
}
74+
75+
fn collect_stats(&self, state: &mut MetricsState) {
76+
let step = state.running_steps.last_mut().unwrap();
77+
78+
let elapsed = state.timer_start.unwrap().elapsed();
79+
step.duration_excluding_children_sec += elapsed;
80+
81+
state.system_info.refresh_cpu();
82+
let cpu = state.system_info.processors().iter().map(|p| p.cpu_usage()).sum::<f32>();
83+
step.cpu_usage_time_sec += cpu as f64 / 100.0 * elapsed.as_secs_f64();
84+
}
85+
86+
pub(crate) fn persist(&self, build: &Build) {
87+
let mut state = self.state.borrow_mut();
88+
assert!(state.running_steps.is_empty(), "steps are still executing");
89+
90+
let dest = build.out.join("metrics.json");
91+
92+
let mut system = System::new();
93+
system.refresh_cpu();
94+
system.refresh_memory();
95+
96+
let system_stats = JsonInvocationSystemStats {
97+
cpu_threads_count: system.processors().len(),
98+
cpu_model: system.processors()[0].brand().into(),
99+
100+
memory_total_bytes: system.total_memory() * 1024,
101+
};
102+
let steps = std::mem::take(&mut state.finished_steps);
103+
104+
// Some of our CI builds consist of multiple independent CI invocations. Ensure all the
105+
// previous invocations are still present in the resulting file.
106+
let mut invocations = match std::fs::read(&dest) {
107+
Ok(contents) => t!(serde_json::from_slice::<JsonRoot>(&contents)).invocations,
108+
Err(err) => {
109+
if err.kind() != std::io::ErrorKind::NotFound {
110+
panic!("failed to open existing metrics file at {}: {err}", dest.display());
111+
}
112+
Vec::new()
113+
}
114+
};
115+
invocations.push(JsonInvocation {
116+
duration_including_children_sec: state.invocation_timer_start.elapsed().as_secs_f64(),
117+
children: steps.into_iter().map(|step| self.prepare_json_step(step)).collect(),
118+
});
119+
120+
let json = JsonRoot { system_stats, invocations };
121+
122+
t!(std::fs::create_dir_all(dest.parent().unwrap()));
123+
let mut file = BufWriter::new(t!(File::create(&dest)));
124+
t!(serde_json::to_writer(&mut file, &json));
125+
}
126+
127+
fn prepare_json_step(&self, step: StepMetrics) -> JsonNode {
128+
JsonNode::RustbuildStep {
129+
type_: step.type_,
130+
debug_repr: step.debug_repr,
131+
132+
duration_excluding_children_sec: step.duration_excluding_children_sec.as_secs_f64(),
133+
system_stats: JsonStepSystemStats {
134+
cpu_utilization_percent: step.cpu_usage_time_sec * 100.0
135+
/ step.duration_excluding_children_sec.as_secs_f64(),
136+
},
137+
138+
children: step
139+
.children
140+
.into_iter()
141+
.map(|child| self.prepare_json_step(child))
142+
.collect(),
143+
}
144+
}
145+
}
146+
147+
struct MetricsState {
148+
finished_steps: Vec<StepMetrics>,
149+
running_steps: Vec<StepMetrics>,
150+
151+
system_info: System,
152+
timer_start: Option<Instant>,
153+
invocation_timer_start: Instant,
154+
}
155+
156+
struct StepMetrics {
157+
type_: String,
158+
debug_repr: String,
159+
160+
cpu_usage_time_sec: f64,
161+
duration_excluding_children_sec: Duration,
162+
163+
children: Vec<StepMetrics>,
164+
}
165+
166+
#[derive(Serialize, Deserialize)]
167+
#[serde(rename_all = "snake_case")]
168+
struct JsonRoot {
169+
system_stats: JsonInvocationSystemStats,
170+
invocations: Vec<JsonInvocation>,
171+
}
172+
173+
#[derive(Serialize, Deserialize)]
174+
#[serde(rename_all = "snake_case")]
175+
struct JsonInvocation {
176+
duration_including_children_sec: f64,
177+
children: Vec<JsonNode>,
178+
}
179+
180+
#[derive(Serialize, Deserialize)]
181+
#[serde(tag = "kind", rename_all = "snake_case")]
182+
enum JsonNode {
183+
RustbuildStep {
184+
#[serde(rename = "type")]
185+
type_: String,
186+
debug_repr: String,
187+
188+
duration_excluding_children_sec: f64,
189+
system_stats: JsonStepSystemStats,
190+
191+
children: Vec<JsonNode>,
192+
},
193+
}
194+
195+
#[derive(Serialize, Deserialize)]
196+
#[serde(rename_all = "snake_case")]
197+
struct JsonInvocationSystemStats {
198+
cpu_threads_count: usize,
199+
cpu_model: String,
200+
201+
memory_total_bytes: u64,
202+
}
203+
204+
#[derive(Serialize, Deserialize)]
205+
#[serde(rename_all = "snake_case")]
206+
struct JsonStepSystemStats {
207+
cpu_utilization_percent: f64,
208+
}

0 commit comments

Comments
 (0)