]> git.lizzy.rs Git - rust.git/blob - crates/rust-analyzer/src/cli/analysis_stats.rs
9445aec074d411294abde492eb5589e526d2a511
[rust.git] / crates / rust-analyzer / src / cli / analysis_stats.rs
1 //! Fully type-check project and print various stats, like the number of type
2 //! errors.
3
4 use std::{
5     path::PathBuf,
6     time::{SystemTime, UNIX_EPOCH},
7 };
8
9 use hir::{
10     db::{AstDatabase, DefDatabase, HirDatabase},
11     AssocItem, Crate, HasSource, HirDisplay, ModuleDef,
12 };
13 use hir_def::FunctionId;
14 use hir_ty::{Ty, TypeWalk};
15 use ide_db::base_db::{
16     salsa::{self, ParallelDatabase},
17     SourceDatabaseExt,
18 };
19 use itertools::Itertools;
20 use oorandom::Rand32;
21 use rayon::prelude::*;
22 use rustc_hash::FxHashSet;
23 use stdx::format_to;
24 use syntax::AstNode;
25
26 use crate::cli::{
27     load_cargo::load_cargo, print_memory_usage, progress_report::ProgressReport, report_metric,
28     Result, Verbosity,
29 };
30 use profile::StopWatch;
31
32 /// Need to wrap Snapshot to provide `Clone` impl for `map_with`
33 struct Snap<DB>(DB);
34 impl<DB: ParallelDatabase> Clone for Snap<salsa::Snapshot<DB>> {
35     fn clone(&self) -> Snap<salsa::Snapshot<DB>> {
36         Snap(self.0.snapshot())
37     }
38 }
39
40 pub struct AnalysisStatsCmd {
41     pub randomize: bool,
42     pub parallel: bool,
43     pub memory_usage: bool,
44     pub only: Option<String>,
45     pub with_deps: bool,
46     pub path: PathBuf,
47     pub load_output_dirs: bool,
48     pub with_proc_macro: bool,
49 }
50
51 impl AnalysisStatsCmd {
52     pub fn run(self, verbosity: Verbosity) -> Result<()> {
53         let mut rng = {
54             let seed = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() as u64;
55             Rand32::new(seed)
56         };
57
58         let mut db_load_sw = self.stop_watch();
59         let (host, vfs) = load_cargo(&self.path, self.load_output_dirs, self.with_proc_macro)?;
60         let db = host.raw_database();
61         eprintln!("Database loaded {}", db_load_sw.elapsed());
62
63         let mut analysis_sw = self.stop_watch();
64         let mut num_crates = 0;
65         let mut visited_modules = FxHashSet::default();
66         let mut visit_queue = Vec::new();
67
68         let mut krates = Crate::all(db);
69         if self.randomize {
70             shuffle(&mut rng, &mut krates);
71         }
72         for krate in krates {
73             let module = krate.root_module(db);
74             let file_id = module.definition_source(db).file_id;
75             let file_id = file_id.original_file(db);
76             let source_root = db.file_source_root(file_id);
77             let source_root = db.source_root(source_root);
78             if !source_root.is_library || self.with_deps {
79                 num_crates += 1;
80                 visit_queue.push(module);
81             }
82         }
83
84         if self.randomize {
85             shuffle(&mut rng, &mut visit_queue);
86         }
87
88         eprintln!("Crates in this dir: {}", num_crates);
89         let mut num_decls = 0;
90         let mut funcs = Vec::new();
91         while let Some(module) = visit_queue.pop() {
92             if visited_modules.insert(module) {
93                 visit_queue.extend(module.children(db));
94
95                 for decl in module.declarations(db) {
96                     num_decls += 1;
97                     if let ModuleDef::Function(f) = decl {
98                         funcs.push(f);
99                     }
100                 }
101
102                 for impl_def in module.impl_defs(db) {
103                     for item in impl_def.items(db) {
104                         num_decls += 1;
105                         if let AssocItem::Function(f) = item {
106                             funcs.push(f);
107                         }
108                     }
109                 }
110             }
111         }
112         eprintln!("Total modules found: {}", visited_modules.len());
113         eprintln!("Total declarations: {}", num_decls);
114         eprintln!("Total functions: {}", funcs.len());
115         eprintln!("Item Collection: {}", analysis_sw.elapsed());
116
117         if self.randomize {
118             shuffle(&mut rng, &mut funcs);
119         }
120
121         let mut bar = match verbosity {
122             Verbosity::Quiet | Verbosity::Spammy => ProgressReport::hidden(),
123             _ if self.parallel => ProgressReport::hidden(),
124             _ => ProgressReport::new(funcs.len() as u64),
125         };
126
127         if self.parallel {
128             let mut inference_sw = self.stop_watch();
129             let snap = Snap(db.snapshot());
130             funcs
131                 .par_iter()
132                 .map_with(snap, |snap, &f| {
133                     let f_id = FunctionId::from(f);
134                     snap.0.body(f_id.into());
135                     snap.0.infer(f_id.into());
136                 })
137                 .count();
138             eprintln!("Parallel Inference: {}", inference_sw.elapsed());
139         }
140
141         let mut inference_sw = self.stop_watch();
142         bar.tick();
143         let mut num_exprs = 0;
144         let mut num_exprs_unknown = 0;
145         let mut num_exprs_partially_unknown = 0;
146         let mut num_type_mismatches = 0;
147         for f in funcs {
148             let name = f.name(db);
149             let full_name = f
150                 .module(db)
151                 .path_to_root(db)
152                 .into_iter()
153                 .rev()
154                 .filter_map(|it| it.name(db))
155                 .chain(Some(f.name(db)))
156                 .join("::");
157             if let Some(only_name) = self.only.as_deref() {
158                 if name.to_string() != only_name && full_name != only_name {
159                     continue;
160                 }
161             }
162             let mut msg = format!("processing: {}", full_name);
163             if verbosity.is_verbose() {
164                 if let Some(src) = f.source(db) {
165                     let original_file = src.file_id.original_file(db);
166                     let path = vfs.file_path(original_file);
167                     let syntax_range = src.value.syntax().text_range();
168                     format_to!(msg, " ({} {:?})", path, syntax_range);
169                 }
170             }
171             if verbosity.is_spammy() {
172                 bar.println(msg.to_string());
173             }
174             bar.set_message(&msg);
175             let f_id = FunctionId::from(f);
176             let body = db.body(f_id.into());
177             let inference_result = db.infer(f_id.into());
178             let (previous_exprs, previous_unknown, previous_partially_unknown) =
179                 (num_exprs, num_exprs_unknown, num_exprs_partially_unknown);
180             for (expr_id, _) in body.exprs.iter() {
181                 let ty = &inference_result[expr_id];
182                 num_exprs += 1;
183                 if let Ty::Unknown = ty {
184                     num_exprs_unknown += 1;
185                 } else {
186                     let mut is_partially_unknown = false;
187                     ty.walk(&mut |ty| {
188                         if let Ty::Unknown = ty {
189                             is_partially_unknown = true;
190                         }
191                     });
192                     if is_partially_unknown {
193                         num_exprs_partially_unknown += 1;
194                     }
195                 }
196                 if self.only.is_some() && verbosity.is_spammy() {
197                     // in super-verbose mode for just one function, we print every single expression
198                     let (_, sm) = db.body_with_source_map(f_id.into());
199                     let src = sm.expr_syntax(expr_id);
200                     if let Ok(src) = src {
201                         let node = {
202                             let root = db.parse_or_expand(src.file_id).unwrap();
203                             src.value.to_node(&root)
204                         };
205                         let original_file = src.file_id.original_file(db);
206                         let line_index = host.analysis().file_line_index(original_file).unwrap();
207                         let text_range = node.syntax().text_range();
208                         let (start, end) = (
209                             line_index.line_col(text_range.start()),
210                             line_index.line_col(text_range.end()),
211                         );
212                         bar.println(format!(
213                             "{}:{}-{}:{}: {}",
214                             start.line + 1,
215                             start.col_utf16,
216                             end.line + 1,
217                             end.col_utf16,
218                             ty.display(db)
219                         ));
220                     } else {
221                         bar.println(format!("unknown location: {}", ty.display(db)));
222                     }
223                 }
224                 if let Some(mismatch) = inference_result.type_mismatch_for_expr(expr_id) {
225                     num_type_mismatches += 1;
226                     if verbosity.is_verbose() {
227                         let (_, sm) = db.body_with_source_map(f_id.into());
228                         let src = sm.expr_syntax(expr_id);
229                         if let Ok(src) = src {
230                             // FIXME: it might be nice to have a function (on Analysis?) that goes from Source<T> -> (LineCol, LineCol) directly
231                             // But also, we should just turn the type mismatches into diagnostics and provide these
232                             let root = db.parse_or_expand(src.file_id).unwrap();
233                             let node = src.map(|e| e.to_node(&root).syntax().clone());
234                             let original_range = node.as_ref().original_file_range(db);
235                             let path = vfs.file_path(original_range.file_id);
236                             let line_index =
237                                 host.analysis().file_line_index(original_range.file_id).unwrap();
238                             let text_range = original_range.range;
239                             let (start, end) = (
240                                 line_index.line_col(text_range.start()),
241                                 line_index.line_col(text_range.end()),
242                             );
243                             bar.println(format!(
244                                 "{} {}:{}-{}:{}: Expected {}, got {}",
245                                 path,
246                                 start.line + 1,
247                                 start.col_utf16,
248                                 end.line + 1,
249                                 end.col_utf16,
250                                 mismatch.expected.display(db),
251                                 mismatch.actual.display(db)
252                             ));
253                         } else {
254                             bar.println(format!(
255                                 "{}: Expected {}, got {}",
256                                 name,
257                                 mismatch.expected.display(db),
258                                 mismatch.actual.display(db)
259                             ));
260                         }
261                     }
262                 }
263             }
264             if verbosity.is_spammy() {
265                 bar.println(format!(
266                     "In {}: {} exprs, {} unknown, {} partial",
267                     full_name,
268                     num_exprs - previous_exprs,
269                     num_exprs_unknown - previous_unknown,
270                     num_exprs_partially_unknown - previous_partially_unknown
271                 ));
272             }
273             bar.inc(1);
274         }
275         bar.finish_and_clear();
276         eprintln!("Total expressions: {}", num_exprs);
277         eprintln!(
278             "Expressions of unknown type: {} ({}%)",
279             num_exprs_unknown,
280             if num_exprs > 0 { num_exprs_unknown * 100 / num_exprs } else { 100 }
281         );
282         report_metric("unknown type", num_exprs_unknown, "#");
283
284         eprintln!(
285             "Expressions of partially unknown type: {} ({}%)",
286             num_exprs_partially_unknown,
287             if num_exprs > 0 { num_exprs_partially_unknown * 100 / num_exprs } else { 100 }
288         );
289
290         eprintln!("Type mismatches: {}", num_type_mismatches);
291         report_metric("type mismatches", num_type_mismatches, "#");
292
293         eprintln!("Inference: {}", inference_sw.elapsed());
294
295         let total_span = analysis_sw.elapsed();
296         eprintln!("Total: {}", total_span);
297         report_metric("total time", total_span.time.as_millis() as u64, "ms");
298         if let Some(instructions) = total_span.instructions {
299             report_metric("total instructions", instructions, "#instr");
300         }
301         if let Some(memory) = total_span.memory {
302             report_metric("total memory", memory.allocated.megabytes() as u64, "MB");
303         }
304
305         if self.memory_usage {
306             print_memory_usage(host, vfs);
307         }
308
309         Ok(())
310     }
311
312     fn stop_watch(&self) -> StopWatch {
313         StopWatch::start().memory(self.memory_usage)
314     }
315 }
316
317 fn shuffle<T>(rng: &mut Rand32, slice: &mut [T]) {
318     for i in 0..slice.len() {
319         randomize_first(rng, &mut slice[i..]);
320     }
321
322     fn randomize_first<T>(rng: &mut Rand32, slice: &mut [T]) {
323         assert!(!slice.is_empty());
324         let idx = rng.rand_range(0..slice.len() as u32) as usize;
325         slice.swap(0, idx);
326     }
327 }