xtask/
metrics.rs

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}