1use std::{
2 collections::BTreeMap,
3 fs,
4 io::Write as _,
5 path::Path,
6 time::{Instant, SystemTime, UNIX_EPOCH},
7};
8
9use anyhow::format_err;
10use xshell::{Shell, cmd};
11
12use crate::flags::{self, MeasurementType};
13
14type Unit = String;
15
16impl flags::Metrics {
17 pub(crate) fn run(self, sh: &Shell) -> anyhow::Result<()> {
18 let mut metrics = Metrics::new(sh)?;
19 if !Path::new("./target/metrics/rustc-perf").exists() {
20 sh.create_dir("./target/metrics/rustc-perf")?;
21 cmd!(
22 sh,
23 "git clone https://github.com/rust-lang/rustc-perf.git ./target/metrics/rustc-perf"
24 )
25 .run()?;
26 }
27 {
28 let _d = sh.push_dir("./target/metrics/rustc-perf");
29 let revision = &metrics.perf_revision;
30 cmd!(sh, "git reset --hard {revision}").run()?;
31 }
32
33 let _env = sh.push_env("RA_METRICS", "1");
34
35 let name = match &self.measurement_type {
36 Some(ms) => {
37 let name = ms.as_ref();
38 match ms {
39 MeasurementType::Build => {
40 metrics.measure_build(sh)?;
41 }
42 MeasurementType::RustcTests => {
43 metrics.measure_rustc_tests(sh)?;
44 }
45 MeasurementType::AnalyzeSelf => {
46 metrics.measure_analysis_stats_self(sh)?;
47 }
48 MeasurementType::AnalyzeRipgrep
49 | MeasurementType::AnalyzeWebRender
50 | MeasurementType::AnalyzeDiesel
51 | MeasurementType::AnalyzeHyper => {
52 metrics.measure_analysis_stats(sh, name)?;
53 }
54 };
55 name
56 }
57 None => {
58 metrics.measure_build(sh)?;
59 metrics.measure_rustc_tests(sh)?;
60 metrics.measure_analysis_stats_self(sh)?;
61 metrics.measure_analysis_stats(sh, MeasurementType::AnalyzeRipgrep.as_ref())?;
62 metrics.measure_analysis_stats(sh, MeasurementType::AnalyzeWebRender.as_ref())?;
63 metrics.measure_analysis_stats(sh, MeasurementType::AnalyzeDiesel.as_ref())?;
64 metrics.measure_analysis_stats(sh, MeasurementType::AnalyzeHyper.as_ref())?;
65 "all"
66 }
67 };
68
69 let mut file =
70 fs::File::options().write(true).create(true).open(format!("target/{name}.json"))?;
71 writeln!(file, "{}", metrics.json())?;
72 eprintln!("{metrics:#?}");
73 Ok(())
74 }
75}
76
77impl Metrics {
78 fn measure_build(&mut self, sh: &Shell) -> anyhow::Result<()> {
79 eprintln!("\nMeasuring build");
80 cmd!(sh, "cargo fetch").run()?;
81
82 let time = Instant::now();
83 cmd!(sh, "cargo build --release --package rust-analyzer --bin rust-analyzer").run()?;
84 let time = time.elapsed();
85 self.report("build", time.as_millis() as u64, "ms".into());
86 Ok(())
87 }
88
89 fn measure_rustc_tests(&mut self, sh: &Shell) -> anyhow::Result<()> {
90 eprintln!("\nMeasuring rustc tests");
91
92 cmd!(
93 sh,
94 "git clone --depth=1 --branch 1.76.0 https://github.com/rust-lang/rust.git --single-branch ./target/metrics/rust"
95 )
96 .run()?;
97
98 let output =
99 cmd!(sh, "./target/release/rust-analyzer rustc-tests ./target/metrics/rust").read()?;
100 for (metric, value, unit) in parse_metrics(&output) {
101 self.report(metric, value, unit.into());
102 }
103 Ok(())
104 }
105
106 fn measure_analysis_stats_self(&mut self, sh: &Shell) -> anyhow::Result<()> {
107 self.measure_analysis_stats_path(sh, "self", ".")
108 }
109 fn measure_analysis_stats(&mut self, sh: &Shell, bench: &str) -> anyhow::Result<()> {
110 self.measure_analysis_stats_path(
111 sh,
112 bench,
113 &format!("./target/metrics/rustc-perf/collector/compile-benchmarks/{bench}"),
114 )
115 }
116 fn measure_analysis_stats_path(
117 &mut self,
118 sh: &Shell,
119 name: &str,
120 path: &str,
121 ) -> anyhow::Result<()> {
122 assert!(Path::new(path).exists(), "unable to find bench in {path}");
123 eprintln!("\nMeasuring analysis-stats/{name}");
124 let output = cmd!(sh, "./target/release/rust-analyzer -q analysis-stats {path}").read()?;
125 for (metric, value, unit) in parse_metrics(&output) {
126 self.report(&format!("analysis-stats/{name}/{metric}"), value, unit.into());
127 }
128 Ok(())
129 }
130}
131
132fn parse_metrics(output: &str) -> Vec<(&str, u64, &str)> {
133 output
134 .lines()
135 .filter_map(|it| {
136 let entry = it.split(':').collect::<Vec<_>>();
137 match entry.as_slice() {
138 ["METRIC", name, value, unit] => Some((*name, value.parse().unwrap(), *unit)),
139 _ => None,
140 }
141 })
142 .collect()
143}
144
145#[derive(Debug)]
146struct Metrics {
147 host: Host,
148 timestamp: SystemTime,
149 revision: String,
150 perf_revision: String,
151 metrics: BTreeMap<String, (u64, Unit)>,
152}
153
154#[derive(Debug)]
155struct Host {
156 os: String,
157 cpu: String,
158 mem: String,
159}
160
161impl Metrics {
162 fn new(sh: &Shell) -> anyhow::Result<Metrics> {
163 let host = Host::new(sh).unwrap_or_else(|_| Host::unknown());
164 let timestamp = SystemTime::now();
165 let revision = cmd!(sh, "git rev-parse HEAD").read()?;
166 let perf_revision = "a584462e145a0c04760fd9391daefb4f6bd13a99".into();
167 Ok(Metrics { host, timestamp, revision, perf_revision, metrics: BTreeMap::new() })
168 }
169
170 fn report(&mut self, name: &str, value: u64, unit: Unit) {
171 self.metrics.insert(name.into(), (value, unit));
172 }
173
174 fn json(&self) -> String {
175 let mut buf = String::new();
176 self.to_json(write_json::object(&mut buf));
177 buf
178 }
179
180 fn to_json(&self, mut obj: write_json::Object<'_>) {
181 self.host.to_json(obj.object("host"));
182 let timestamp = self.timestamp.duration_since(UNIX_EPOCH).unwrap();
183 obj.number("timestamp", timestamp.as_secs() as f64);
184 obj.string("revision", &self.revision);
185 obj.string("perf_revision", &self.perf_revision);
186 let mut metrics = obj.object("metrics");
187 for (k, (value, unit)) in &self.metrics {
188 metrics.array(k).number(*value as f64).string(unit);
189 }
190 }
191}
192
193impl Host {
194 fn unknown() -> Host {
195 Host { os: "unknown".into(), cpu: "unknown".into(), mem: "unknown".into() }
196 }
197
198 fn new(sh: &Shell) -> anyhow::Result<Host> {
199 if cfg!(not(target_os = "linux")) {
200 return Ok(Host::unknown());
201 }
202
203 let os = read_field(sh, "/etc/os-release", "PRETTY_NAME=")?.trim_matches('"').to_owned();
204
205 let cpu = read_field(sh, "/proc/cpuinfo", "model name")?
206 .trim_start_matches(':')
207 .trim()
208 .to_owned();
209
210 let mem = read_field(sh, "/proc/meminfo", "MemTotal:")?;
211
212 return Ok(Host { os, cpu, mem });
213
214 fn read_field(sh: &Shell, path: &str, field: &str) -> anyhow::Result<String> {
215 let text = sh.read_file(path)?;
216
217 text.lines()
218 .find_map(|it| it.strip_prefix(field))
219 .map(|it| it.trim().to_owned())
220 .ok_or_else(|| format_err!("can't parse {}", path))
221 }
222 }
223 fn to_json(&self, mut obj: write_json::Object<'_>) {
224 obj.string("os", &self.os).string("cpu", &self.cpu).string("mem", &self.mem);
225 }
226}