]> git.lizzy.rs Git - rust.git/blob - xtask/src/metrics.rs
Merge #11535
[rust.git] / xtask / src / metrics.rs
1 use std::{
2     collections::BTreeMap,
3     env,
4     io::Write as _,
5     path::Path,
6     time::{Instant, SystemTime, UNIX_EPOCH},
7 };
8
9 use anyhow::{bail, format_err, Result};
10 use xshell::{cmd, mkdir_p, pushd, pushenv, read_file, rm_rf};
11
12 use crate::flags;
13
14 type Unit = String;
15
16 impl flags::Metrics {
17     pub(crate) fn run(self) -> Result<()> {
18         let mut metrics = Metrics::new()?;
19         if !self.dry_run {
20             rm_rf("./target/release")?;
21         }
22         if !Path::new("./target/rustc-perf").exists() {
23             mkdir_p("./target/rustc-perf")?;
24             cmd!("git clone https://github.com/rust-lang/rustc-perf.git ./target/rustc-perf")
25                 .run()?;
26         }
27         {
28             let _d = pushd("./target/rustc-perf")?;
29             let revision = &metrics.perf_revision;
30             cmd!("git reset --hard {revision}").run()?;
31         }
32
33         let _env = pushenv("RA_METRICS", "1");
34
35         metrics.measure_build()?;
36         metrics.measure_analysis_stats_self()?;
37         metrics.measure_analysis_stats("ripgrep")?;
38         metrics.measure_analysis_stats("webrender")?;
39         metrics.measure_analysis_stats("diesel/diesel")?;
40
41         if !self.dry_run {
42             let _d = pushd("target")?;
43             let metrics_token = env::var("METRICS_TOKEN").unwrap();
44             cmd!(
45                 "git clone --depth 1 https://{metrics_token}@github.com/rust-analyzer/metrics.git"
46             )
47             .run()?;
48             let _d = pushd("metrics")?;
49
50             let mut file = std::fs::OpenOptions::new().append(true).open("metrics.json")?;
51             writeln!(file, "{}", metrics.json())?;
52             cmd!("git add .").run()?;
53             cmd!("git -c user.name=Bot -c user.email=dummy@example.com commit --message ðŸ“ˆ")
54                 .run()?;
55             cmd!("git push origin master").run()?;
56         }
57         eprintln!("{:#?}", metrics);
58         Ok(())
59     }
60 }
61
62 impl Metrics {
63     fn measure_build(&mut self) -> Result<()> {
64         eprintln!("\nMeasuring build");
65         cmd!("cargo fetch").run()?;
66
67         let time = Instant::now();
68         cmd!("cargo build --release --package rust-analyzer --bin rust-analyzer").run()?;
69         let time = time.elapsed();
70         self.report("build", time.as_millis() as u64, "ms".into());
71         Ok(())
72     }
73     fn measure_analysis_stats_self(&mut self) -> Result<()> {
74         self.measure_analysis_stats_path("self", ".")
75     }
76     fn measure_analysis_stats(&mut self, bench: &str) -> Result<()> {
77         self.measure_analysis_stats_path(
78             bench,
79             &format!("./target/rustc-perf/collector/benchmarks/{}", bench),
80         )
81     }
82     fn measure_analysis_stats_path(&mut self, name: &str, path: &str) -> Result<()> {
83         eprintln!("\nMeasuring analysis-stats/{}", name);
84         let output = cmd!("./target/release/rust-analyzer -q analysis-stats --memory-usage {path}")
85             .read()?;
86         for (metric, value, unit) in parse_metrics(&output) {
87             self.report(&format!("analysis-stats/{}/{}", name, metric), value, unit.into());
88         }
89         Ok(())
90     }
91 }
92
93 fn parse_metrics(output: &str) -> Vec<(&str, u64, &str)> {
94     output
95         .lines()
96         .filter_map(|it| {
97             let entry = it.split(':').collect::<Vec<_>>();
98             match entry.as_slice() {
99                 ["METRIC", name, value, unit] => Some((*name, value.parse().unwrap(), *unit)),
100                 _ => None,
101             }
102         })
103         .collect()
104 }
105
106 #[derive(Debug)]
107 struct Metrics {
108     host: Host,
109     timestamp: SystemTime,
110     revision: String,
111     perf_revision: String,
112     metrics: BTreeMap<String, (u64, Unit)>,
113 }
114
115 #[derive(Debug)]
116 struct Host {
117     os: String,
118     cpu: String,
119     mem: String,
120 }
121
122 impl Metrics {
123     fn new() -> Result<Metrics> {
124         let host = Host::new()?;
125         let timestamp = SystemTime::now();
126         let revision = cmd!("git rev-parse HEAD").read()?;
127         let perf_revision = "c52ee623e231e7690a93be88d943016968c1036b".into();
128         Ok(Metrics { host, timestamp, revision, perf_revision, metrics: BTreeMap::new() })
129     }
130
131     fn report(&mut self, name: &str, value: u64, unit: Unit) {
132         self.metrics.insert(name.into(), (value, unit));
133     }
134
135     fn json(&self) -> String {
136         let mut buf = String::new();
137         self.to_json(write_json::object(&mut buf));
138         buf
139     }
140
141     fn to_json(&self, mut obj: write_json::Object<'_>) {
142         self.host.to_json(obj.object("host"));
143         let timestamp = self.timestamp.duration_since(UNIX_EPOCH).unwrap();
144         obj.number("timestamp", timestamp.as_secs() as f64);
145         obj.string("revision", &self.revision);
146         obj.string("perf_revision", &self.perf_revision);
147         let mut metrics = obj.object("metrics");
148         for (k, (value, unit)) in &self.metrics {
149             metrics.array(k).number(*value as f64).string(unit);
150         }
151     }
152 }
153
154 impl Host {
155     fn new() -> Result<Host> {
156         if cfg!(not(target_os = "linux")) {
157             bail!("can only collect metrics on Linux ");
158         }
159
160         let os = read_field("/etc/os-release", "PRETTY_NAME=")?.trim_matches('"').to_string();
161
162         let cpu =
163             read_field("/proc/cpuinfo", "model name")?.trim_start_matches(':').trim().to_string();
164
165         let mem = read_field("/proc/meminfo", "MemTotal:")?;
166
167         return Ok(Host { os, cpu, mem });
168
169         fn read_field(path: &str, field: &str) -> Result<String> {
170             let text = read_file(path)?;
171
172             let line = text
173                 .lines()
174                 .find(|it| it.starts_with(field))
175                 .ok_or_else(|| format_err!("can't parse {}", path))?;
176             Ok(line[field.len()..].trim().to_string())
177         }
178     }
179     fn to_json(&self, mut obj: write_json::Object<'_>) {
180         obj.string("os", &self.os).string("cpu", &self.cpu).string("mem", &self.mem);
181     }
182 }