diff options
Diffstat (limited to 'xtask/src/metrics.rs')
-rw-r--r-- | xtask/src/metrics.rs | 279 |
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 @@ | |||
1 | use 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 | |||
10 | use anyhow::{bail, format_err, Result}; | ||
11 | |||
12 | use crate::not_bash::{fs2, pushd, pushenv, rm_rf, run}; | ||
13 | |||
14 | type Unit = String; | ||
15 | |||
16 | pub struct MetricsCmd { | ||
17 | pub dry_run: bool, | ||
18 | } | ||
19 | |||
20 | impl 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 | |||
60 | impl 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 | |||
90 | fn 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)] | ||
104 | struct Metrics { | ||
105 | host: Host, | ||
106 | timestamp: SystemTime, | ||
107 | revision: String, | ||
108 | metrics: BTreeMap<String, (u64, Unit)>, | ||
109 | } | ||
110 | |||
111 | #[derive(Debug)] | ||
112 | struct Host { | ||
113 | os: String, | ||
114 | cpu: String, | ||
115 | mem: String, | ||
116 | } | ||
117 | |||
118 | impl 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 | |||
167 | impl 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 | |||
208 | struct State { | ||
209 | obj: bool, | ||
210 | first: bool, | ||
211 | } | ||
212 | |||
213 | #[derive(Default)] | ||
214 | struct Json { | ||
215 | stack: Vec<State>, | ||
216 | buf: String, | ||
217 | } | ||
218 | |||
219 | impl 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 | |||
275 | impl fmt::Display for Json { | ||
276 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
277 | write!(f, "{}", self.buf) | ||
278 | } | ||
279 | } | ||