aboutsummaryrefslogtreecommitdiff
path: root/xtask/src/metrics.rs
diff options
context:
space:
mode:
Diffstat (limited to 'xtask/src/metrics.rs')
-rw-r--r--xtask/src/metrics.rs279
1 files changed, 279 insertions, 0 deletions
diff --git a/xtask/src/metrics.rs b/xtask/src/metrics.rs
new file mode 100644
index 000000000..9ac3fa51d
--- /dev/null
+++ b/xtask/src/metrics.rs
@@ -0,0 +1,279 @@
1use std::{
2 collections::BTreeMap,
3 env,
4 fmt::{self, Write as _},
5 io::Write as _,
6 path::Path,
7 time::{Instant, SystemTime, UNIX_EPOCH},
8};
9
10use anyhow::{bail, format_err, Result};
11
12use crate::not_bash::{fs2, pushd, pushenv, rm_rf, run};
13
14type Unit = String;
15
16pub struct MetricsCmd {
17 pub dry_run: bool,
18}
19
20impl MetricsCmd {
21 pub fn run(self) -> Result<()> {
22 let mut metrics = Metrics::new()?;
23 if !self.dry_run {
24 rm_rf("./target/release")?;
25 }
26 if !Path::new("./target/rustc-perf").exists() {
27 fs2::create_dir_all("./target/rustc-perf")?;
28 run!("git clone https://github.com/rust-lang/rustc-perf.git ./target/rustc-perf")?;
29 }
30 {
31 let _d = pushd("./target/rustc-perf");
32 run!("git reset --hard 1d9288b0da7febf2599917da1b57dc241a1af033")?;
33 }
34
35 let _env = pushenv("RA_METRICS", "1");
36
37 metrics.measure_build()?;
38 metrics.measure_analysis_stats_self()?;
39 metrics.measure_analysis_stats("ripgrep")?;
40 metrics.measure_analysis_stats("webrender")?;
41
42 if !self.dry_run {
43 let _d = pushd("target");
44 let metrics_token = env::var("METRICS_TOKEN").unwrap();
45 let repo = format!("https://{}@github.com/rust-analyzer/metrics.git", metrics_token);
46 run!("git clone --depth 1 {}", repo)?;
47 let _d = pushd("metrics");
48
49 let mut file = std::fs::OpenOptions::new().append(true).open("metrics.json")?;
50 writeln!(file, "{}", metrics.json())?;
51 run!("git add .")?;
52 run!("git -c user.name=Bot -c [email protected] commit --message 📈")?;
53 run!("git push origin master")?;
54 }
55 eprintln!("{:#?}", metrics);
56 Ok(())
57 }
58}
59
60impl Metrics {
61 fn measure_build(&mut self) -> Result<()> {
62 eprintln!("\nMeasuring build");
63 run!("cargo fetch")?;
64
65 let time = Instant::now();
66 run!("cargo build --release --package rust-analyzer --bin rust-analyzer")?;
67 let time = time.elapsed();
68 self.report("build", time.as_millis() as u64, "ms".into());
69 Ok(())
70 }
71 fn measure_analysis_stats_self(&mut self) -> Result<()> {
72 self.measure_analysis_stats_path("self", &".")
73 }
74 fn measure_analysis_stats(&mut self, bench: &str) -> Result<()> {
75 self.measure_analysis_stats_path(
76 bench,
77 &format!("./target/rustc-perf/collector/benchmarks/{}", bench),
78 )
79 }
80 fn measure_analysis_stats_path(&mut self, name: &str, path: &str) -> Result<()> {
81 eprintln!("\nMeasuring analysis-stats/{}", name);
82 let output = run!("./target/release/rust-analyzer analysis-stats --quiet {}", path)?;
83 for (metric, value, unit) in parse_metrics(&output) {
84 self.report(&format!("analysis-stats/{}/{}", name, metric), value, unit.into());
85 }
86 Ok(())
87 }
88}
89
90fn parse_metrics(output: &str) -> Vec<(&str, u64, &str)> {
91 output
92 .lines()
93 .filter_map(|it| {
94 let entry = it.split(':').collect::<Vec<_>>();
95 match entry.as_slice() {
96 ["METRIC", name, value, unit] => Some((*name, value.parse().unwrap(), *unit)),
97 _ => None,
98 }
99 })
100 .collect()
101}
102
103#[derive(Debug)]
104struct Metrics {
105 host: Host,
106 timestamp: SystemTime,
107 revision: String,
108 metrics: BTreeMap<String, (u64, Unit)>,
109}
110
111#[derive(Debug)]
112struct Host {
113 os: String,
114 cpu: String,
115 mem: String,
116}
117
118impl Metrics {
119 fn new() -> Result<Metrics> {
120 let host = Host::new()?;
121 let timestamp = SystemTime::now();
122 let revision = run!("git rev-parse HEAD")?;
123 Ok(Metrics { host, timestamp, revision, metrics: BTreeMap::new() })
124 }
125
126 fn report(&mut self, name: &str, value: u64, unit: Unit) {
127 self.metrics.insert(name.into(), (value, unit));
128 }
129
130 fn json(&self) -> Json {
131 let mut json = Json::default();
132 self.to_json(&mut json);
133 json
134 }
135 fn to_json(&self, json: &mut Json) {
136 json.begin_object();
137 {
138 json.field("host");
139 self.host.to_json(json);
140
141 json.field("timestamp");
142 let timestamp = self.timestamp.duration_since(UNIX_EPOCH).unwrap();
143 json.number(timestamp.as_secs() as f64);
144
145 json.field("revision");
146 json.string(&self.revision);
147
148 json.field("metrics");
149 json.begin_object();
150 {
151 for (k, (value, unit)) in &self.metrics {
152 json.field(k);
153 json.begin_array();
154 {
155 json.number(*value as f64);
156 json.string(unit);
157 }
158 json.end_array();
159 }
160 }
161 json.end_object()
162 }
163 json.end_object();
164 }
165}
166
167impl Host {
168 fn new() -> Result<Host> {
169 if cfg!(not(target_os = "linux")) {
170 bail!("can only collect metrics on Linux ");
171 }
172
173 let os = read_field("/etc/os-release", "PRETTY_NAME=")?.trim_matches('"').to_string();
174
175 let cpu =
176 read_field("/proc/cpuinfo", "model name")?.trim_start_matches(':').trim().to_string();
177
178 let mem = read_field("/proc/meminfo", "MemTotal:")?;
179
180 return Ok(Host { os, cpu, mem });
181
182 fn read_field<'a>(path: &str, field: &str) -> Result<String> {
183 let text = fs2::read_to_string(path)?;
184
185 let line = text
186 .lines()
187 .find(|it| it.starts_with(field))
188 .ok_or_else(|| format_err!("can't parse {}", path))?;
189 Ok(line[field.len()..].trim().to_string())
190 }
191 }
192 fn to_json(&self, json: &mut Json) {
193 json.begin_object();
194 {
195 json.field("os");
196 json.string(&self.os);
197
198 json.field("cpu");
199 json.string(&self.cpu);
200
201 json.field("mem");
202 json.string(&self.mem);
203 }
204 json.end_object();
205 }
206}
207
208struct State {
209 obj: bool,
210 first: bool,
211}
212
213#[derive(Default)]
214struct Json {
215 stack: Vec<State>,
216 buf: String,
217}
218
219impl Json {
220 fn begin_object(&mut self) {
221 self.stack.push(State { obj: true, first: true });
222 self.buf.push('{');
223 }
224 fn end_object(&mut self) {
225 self.stack.pop();
226 self.buf.push('}')
227 }
228 fn begin_array(&mut self) {
229 self.stack.push(State { obj: false, first: true });
230 self.buf.push('[');
231 }
232 fn end_array(&mut self) {
233 self.stack.pop();
234 self.buf.push(']')
235 }
236 fn field(&mut self, name: &str) {
237 self.object_comma();
238 self.string_token(name);
239 self.buf.push(':');
240 }
241 fn string(&mut self, value: &str) {
242 self.array_comma();
243 self.string_token(value);
244 }
245 fn string_token(&mut self, value: &str) {
246 self.buf.push('"');
247 self.buf.extend(value.escape_default());
248 self.buf.push('"');
249 }
250 fn number(&mut self, value: f64) {
251 self.array_comma();
252 write!(self.buf, "{}", value).unwrap();
253 }
254
255 fn array_comma(&mut self) {
256 let state = self.stack.last_mut().unwrap();
257 if state.obj {
258 return;
259 }
260 if !state.first {
261 self.buf.push(',');
262 }
263 state.first = false;
264 }
265
266 fn object_comma(&mut self) {
267 let state = self.stack.last_mut().unwrap();
268 if !state.first {
269 self.buf.push(',');
270 }
271 state.first = false;
272 }
273}
274
275impl fmt::Display for Json {
276 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
277 write!(f, "{}", self.buf)
278 }
279}