1 //! Fully type-check project and print various stats, like the number of type
6 time::{SystemTime, UNIX_EPOCH},
10 db::{AstDatabase, DefDatabase, HirDatabase},
11 AssocItem, Crate, HasSource, HirDisplay, ModuleDef,
13 use hir_def::FunctionId;
14 use hir_ty::{Ty, TypeWalk};
15 use ide_db::base_db::{
16 salsa::{self, ParallelDatabase},
19 use itertools::Itertools;
21 use rayon::prelude::*;
22 use rustc_hash::FxHashSet;
27 load_cargo::load_cargo, print_memory_usage, progress_report::ProgressReport, report_metric,
30 use profile::StopWatch;
32 /// Need to wrap Snapshot to provide `Clone` impl for `map_with`
34 impl<DB: ParallelDatabase> Clone for Snap<salsa::Snapshot<DB>> {
35 fn clone(&self) -> Snap<salsa::Snapshot<DB>> {
36 Snap(self.0.snapshot())
40 pub struct AnalysisStatsCmd {
43 pub memory_usage: bool,
44 pub only: Option<String>,
47 pub load_output_dirs: bool,
48 pub with_proc_macro: bool,
51 impl AnalysisStatsCmd {
52 pub fn run(self, verbosity: Verbosity) -> Result<()> {
54 let seed = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() as u64;
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!("{:<20} {}", "Database loaded:", db_load_sw.elapsed());
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();
68 let mut krates = Crate::all(db);
70 shuffle(&mut rng, &mut 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 {
80 visit_queue.push(module);
85 shuffle(&mut rng, &mut visit_queue);
88 eprint!(" crates: {}", 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));
95 for decl in module.declarations(db) {
97 if let ModuleDef::Function(f) = decl {
102 for impl_def in module.impl_defs(db) {
103 for item in impl_def.items(db) {
105 if let AssocItem::Function(f) = item {
112 eprintln!(", mods: {}, decls: {}, fns: {}", visited_modules.len(), num_decls, funcs.len());
113 eprintln!("{:<20} {}", "Item Collection:", analysis_sw.elapsed());
116 shuffle(&mut rng, &mut funcs);
119 let mut bar = match verbosity {
120 Verbosity::Quiet | Verbosity::Spammy => ProgressReport::hidden(),
121 _ if self.parallel => ProgressReport::hidden(),
122 _ => ProgressReport::new(funcs.len() as u64),
126 let mut inference_sw = self.stop_watch();
127 let snap = Snap(db.snapshot());
130 .map_with(snap, |snap, &f| {
131 let f_id = FunctionId::from(f);
132 snap.0.body(f_id.into());
133 snap.0.infer(f_id.into());
136 eprintln!("{:<20} {}", "Parallel Inference:", inference_sw.elapsed());
139 let mut inference_sw = self.stop_watch();
141 let mut num_exprs = 0;
142 let mut num_exprs_unknown = 0;
143 let mut num_exprs_partially_unknown = 0;
144 let mut num_type_mismatches = 0;
146 let name = f.name(db);
152 .filter_map(|it| it.name(db))
153 .chain(Some(f.name(db)))
155 if let Some(only_name) = self.only.as_deref() {
156 if name.to_string() != only_name && full_name != only_name {
160 let mut msg = format!("processing: {}", full_name);
161 if verbosity.is_verbose() {
162 if let Some(src) = f.source(db) {
163 let original_file = src.file_id.original_file(db);
164 let path = vfs.file_path(original_file);
165 let syntax_range = src.value.syntax().text_range();
166 format_to!(msg, " ({} {:?})", path, syntax_range);
169 if verbosity.is_spammy() {
170 bar.println(msg.to_string());
172 bar.set_message(&msg);
173 let f_id = FunctionId::from(f);
174 let body = db.body(f_id.into());
175 let inference_result = db.infer(f_id.into());
176 let (previous_exprs, previous_unknown, previous_partially_unknown) =
177 (num_exprs, num_exprs_unknown, num_exprs_partially_unknown);
178 for (expr_id, _) in body.exprs.iter() {
179 let ty = &inference_result[expr_id];
181 if let Ty::Unknown = ty {
182 num_exprs_unknown += 1;
184 let mut is_partially_unknown = false;
186 if let Ty::Unknown = ty {
187 is_partially_unknown = true;
190 if is_partially_unknown {
191 num_exprs_partially_unknown += 1;
194 if self.only.is_some() && verbosity.is_spammy() {
195 // in super-verbose mode for just one function, we print every single expression
196 let (_, sm) = db.body_with_source_map(f_id.into());
197 let src = sm.expr_syntax(expr_id);
198 if let Ok(src) = src {
200 let root = db.parse_or_expand(src.file_id).unwrap();
201 src.value.to_node(&root)
203 let original_file = src.file_id.original_file(db);
204 let line_index = host.analysis().file_line_index(original_file).unwrap();
205 let text_range = node.syntax().text_range();
207 line_index.line_col(text_range.start()),
208 line_index.line_col(text_range.end()),
219 bar.println(format!("unknown location: {}", ty.display(db)));
222 if let Some(mismatch) = inference_result.type_mismatch_for_expr(expr_id) {
223 num_type_mismatches += 1;
224 if verbosity.is_verbose() {
225 let (_, sm) = db.body_with_source_map(f_id.into());
226 let src = sm.expr_syntax(expr_id);
227 if let Ok(src) = src {
228 // FIXME: it might be nice to have a function (on Analysis?) that goes from Source<T> -> (LineCol, LineCol) directly
229 // But also, we should just turn the type mismatches into diagnostics and provide these
230 let root = db.parse_or_expand(src.file_id).unwrap();
231 let node = src.map(|e| e.to_node(&root).syntax().clone());
232 let original_range = node.as_ref().original_file_range(db);
233 let path = vfs.file_path(original_range.file_id);
235 host.analysis().file_line_index(original_range.file_id).unwrap();
236 let text_range = original_range.range;
238 line_index.line_col(text_range.start()),
239 line_index.line_col(text_range.end()),
242 "{} {}:{}-{}:{}: Expected {}, got {}",
248 mismatch.expected.display(db),
249 mismatch.actual.display(db)
253 "{}: Expected {}, got {}",
255 mismatch.expected.display(db),
256 mismatch.actual.display(db)
262 if verbosity.is_spammy() {
264 "In {}: {} exprs, {} unknown, {} partial",
266 num_exprs - previous_exprs,
267 num_exprs_unknown - previous_unknown,
268 num_exprs_partially_unknown - previous_partially_unknown
273 bar.finish_and_clear();
275 " exprs: {}, ??ty: {} ({}%), ?ty: {} ({}%), !ty: {}",
278 percentage(num_exprs_unknown, num_exprs),
279 num_exprs_partially_unknown,
280 percentage(num_exprs_partially_unknown, num_exprs),
283 report_metric("unknown type", num_exprs_unknown, "#");
284 report_metric("type mismatches", num_type_mismatches, "#");
286 eprintln!("{:<20} {}", "Inference:", inference_sw.elapsed());
288 let total_span = analysis_sw.elapsed();
289 eprintln!("{:<20} {}", "Total:", total_span);
290 report_metric("total time", total_span.time.as_millis() as u64, "ms");
291 if let Some(instructions) = total_span.instructions {
292 report_metric("total instructions", instructions, "#instr");
294 if let Some(memory) = total_span.memory {
295 report_metric("total memory", memory.allocated.megabytes() as u64, "MB");
298 if self.memory_usage && verbosity.is_verbose() {
299 print_memory_usage(host, vfs);
305 fn stop_watch(&self) -> StopWatch {
306 StopWatch::start().memory(self.memory_usage)
310 fn shuffle<T>(rng: &mut Rand32, slice: &mut [T]) {
311 for i in 0..slice.len() {
312 randomize_first(rng, &mut slice[i..]);
315 fn randomize_first<T>(rng: &mut Rand32, slice: &mut [T]) {
316 assert!(!slice.is_empty());
317 let idx = rng.rand_range(0..slice.len() as u32) as usize;
322 fn percentage(n: u64, total: u64) -> u64 {
323 (n * 100).checked_div(total).unwrap_or(100)