Unnamed repository; edit this file 'description' to name the repository.
| -rw-r--r-- | crates/ide-db/src/prime_caches.rs | 42 | ||||
| -rw-r--r-- | crates/ide/src/lib.rs | 14 | ||||
| -rw-r--r-- | crates/ide/src/syntax_highlighting/tests.rs | 2 | ||||
| -rw-r--r-- | crates/load-cargo/src/lib.rs | 3 | ||||
| -rw-r--r-- | crates/project-model/src/project_json.rs | 2 | ||||
| -rw-r--r-- | crates/rust-analyzer/src/cli/prime_caches.rs | 3 | ||||
| -rw-r--r-- | crates/rust-analyzer/src/global_state.rs | 77 | ||||
| -rw-r--r-- | crates/rust-analyzer/src/lib.rs | 1 | ||||
| -rw-r--r-- | crates/rust-analyzer/src/main_loop.rs | 5 | ||||
| -rw-r--r-- | crates/rust-analyzer/src/priming_scope.rs | 166 |
10 files changed, 294 insertions, 21 deletions
diff --git a/crates/ide-db/src/prime_caches.rs b/crates/ide-db/src/prime_caches.rs index fb7edb1acd..518387514c 100644 --- a/crates/ide-db/src/prime_caches.rs +++ b/crates/ide-db/src/prime_caches.rs @@ -4,9 +4,8 @@ //! various caches, it's not really advanced at the moment. use std::panic::AssertUnwindSafe; -use base_db::all_crates; use hir::{Symbol, import_map::ImportMap, sym}; -use rustc_hash::FxHashMap; +use rustc_hash::{FxHashMap, FxHashSet}; use salsa::{Cancelled, Database}; use crate::{FxIndexMap, RootDatabase, base_db::Crate, symbol_index::SymbolIndex}; @@ -23,12 +22,22 @@ pub struct ParallelPrimeCachesProgress { pub work_type: &'static str, } +/// Warm caches for `scope`. +/// +/// `scope` must be closed under transitive dependencies: the scheduler only +/// follows reverse-dep edges within `scope`. Out-of-scope crates can still be +/// primed by salsa on demand when a scope crate's queries reach into them. +/// Callers that want to prime everything pass `&all_crates(db)`. pub fn parallel_prime_caches( db: &RootDatabase, + scope: &[Crate], num_worker_threads: usize, cb: &(dyn Fn(ParallelPrimeCachesProgress) + Sync), ) { - let _p = tracing::info_span!("parallel_prime_caches").entered(); + if scope.is_empty() { + return; + } + let _p = tracing::info_span!("parallel_prime_caches", scope_size = scope.len()).entered(); enum ParallelPrimeCacheWorkerProgress { BeginCrateDefMap { crate_id: Crate, crate_name: Symbol }, @@ -52,17 +61,30 @@ pub fn parallel_prime_caches( // Such def map will just block on the dependency, which is just wasted time. So better // to compute the symbols/import map of an already computed def map in that time. + let scope_set: FxHashSet<Crate> = scope.iter().copied().collect(); + let (reverse_deps, mut to_be_done_deps) = { - let all_crates = all_crates(db); - let to_be_done_deps = all_crates + // Only count in-scope deps — otherwise an out-of-scope dep would + // leave the scheduler waiting on a crate it never enqueued. + let to_be_done_deps = scope .iter() - .map(|&krate| (krate, krate.data(db).dependencies.len() as u32)) + .map(|&krate| { + let count = krate + .data(db) + .dependencies + .iter() + .filter(|dep| scope_set.contains(&dep.crate_id)) + .count() as u32; + (krate, count) + }) .collect::<FxHashMap<_, _>>(); let mut reverse_deps = - all_crates.iter().map(|&krate| (krate, Vec::new())).collect::<FxHashMap<_, _>>(); - for &krate in &*all_crates { + scope.iter().map(|&krate| (krate, Vec::new())).collect::<FxHashMap<_, _>>(); + for &krate in scope { for dep in &krate.data(db).dependencies { - reverse_deps.get_mut(&dep.crate_id).unwrap().push(krate); + if let Some(rev) = reverse_deps.get_mut(&dep.crate_id) { + rev.push(krate); + } } } (reverse_deps, to_be_done_deps) @@ -197,7 +219,7 @@ pub fn parallel_prime_caches( ) }; - let crate_def_maps_total = all_crates(db).len(); + let crate_def_maps_total = scope.len(); let mut crate_def_maps_done = 0; let (mut crate_import_maps_total, mut crate_import_maps_done) = (0usize, 0usize); let (mut module_symbols_total, mut module_symbols_done) = (0usize, 0usize); diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs index dded01520f..c4a3ec1e8e 100644 --- a/crates/ide/src/lib.rs +++ b/crates/ide/src/lib.rs @@ -333,11 +333,21 @@ impl Analysis { }) } - pub fn parallel_prime_caches<F>(&self, num_worker_threads: usize, cb: F) -> Cancellable<()> + /// Warm caches for the given `scope`. `scope` must be closed under + /// transitive dependencies; callers that want to prime everything pass + /// `&base_db::all_crates(db)`. + pub fn parallel_prime_caches<F>( + &self, + scope: &[Crate], + num_worker_threads: usize, + cb: F, + ) -> Cancellable<()> where F: Fn(ParallelPrimeCachesProgress) + Sync + std::panic::UnwindSafe, { - self.with_db(move |db| prime_caches::parallel_prime_caches(db, num_worker_threads, &cb)) + self.with_db(move |db| { + prime_caches::parallel_prime_caches(db, scope, num_worker_threads, &cb) + }) } /// Gets the text of the source file. diff --git a/crates/ide/src/syntax_highlighting/tests.rs b/crates/ide/src/syntax_highlighting/tests.rs index 15e48fecee..f4b1039024 100644 --- a/crates/ide/src/syntax_highlighting/tests.rs +++ b/crates/ide/src/syntax_highlighting/tests.rs @@ -447,7 +447,7 @@ macro_rules! void_2024 { } "#, - expect_file![format!("./test_data/highlight_keywords_macros.html")], + expect_file!["./test_data/highlight_keywords_macros.html"], false, ); } diff --git a/crates/load-cargo/src/lib.rs b/crates/load-cargo/src/lib.rs index f5c5cb432d..bf2331a40f 100644 --- a/crates/load-cargo/src/lib.rs +++ b/crates/load-cargo/src/lib.rs @@ -197,7 +197,8 @@ pub fn load_workspace_into_db( ); if load_config.prefill_caches { - prime_caches::parallel_prime_caches(db, load_config.num_worker_threads, &|_| ()); + let all = ide_db::base_db::all_crates(db); + prime_caches::parallel_prime_caches(db, &all, load_config.num_worker_threads, &|_| ()); } Ok((vfs, proc_macro_server.and_then(Result::ok))) diff --git a/crates/project-model/src/project_json.rs b/crates/project-model/src/project_json.rs index 4ea136afbb..4e2c1e846c 100644 --- a/crates/project-model/src/project_json.rs +++ b/crates/project-model/src/project_json.rs @@ -267,7 +267,7 @@ pub struct Crate { // Extra crate-level attributes, without the surrounding `#![]`. pub(crate) crate_attrs: Vec<String>, pub(crate) proc_macro_dylib_path: Option<AbsPathBuf>, - pub(crate) is_workspace_member: bool, + pub is_workspace_member: bool, pub(crate) include: Vec<AbsPathBuf>, pub(crate) exclude: Vec<AbsPathBuf>, pub(crate) is_proc_macro: bool, diff --git a/crates/rust-analyzer/src/cli/prime_caches.rs b/crates/rust-analyzer/src/cli/prime_caches.rs index beedcfae4e..782b0bda0a 100644 --- a/crates/rust-analyzer/src/cli/prime_caches.rs +++ b/crates/rust-analyzer/src/cli/prime_caches.rs @@ -55,7 +55,8 @@ impl flags::PrimeCaches { ); let threads = self.num_threads.unwrap_or_else(num_cpus::get_physical); - ide_db::prime_caches::parallel_prime_caches(&db, threads, &|_| ()); + let all = ide_db::base_db::all_crates(&db); + ide_db::prime_caches::parallel_prime_caches(&db, &all, threads, &|_| ()); let elapsed = stop_watch.elapsed(); eprintln!( diff --git a/crates/rust-analyzer/src/global_state.rs b/crates/rust-analyzer/src/global_state.rs index 5388f68f02..76a79ae08d 100644 --- a/crates/rust-analyzer/src/global_state.rs +++ b/crates/rust-analyzer/src/global_state.rs @@ -14,7 +14,7 @@ use hir::ChangeWithProcMacros; use ide::{Analysis, AnalysisHost, Cancellable, FileId, SourceRootId}; use ide_db::{ MiniCore, - base_db::{Crate, ProcMacroPaths, SourceDatabase, salsa::Revision}, + base_db::{Crate, ProcMacroPaths, SourceDatabase, all_crates, salsa::Revision}, }; use itertools::Itertools; use load_cargo::SourceRootConfig; @@ -24,7 +24,9 @@ use parking_lot::{ RwLockWriteGuard, }; use proc_macro_api::ProcMacroClient; -use project_model::{ManifestPath, ProjectWorkspace, ProjectWorkspaceKind, WorkspaceBuildScripts}; +use project_model::{ + ManifestPath, ProjectWorkspace, ProjectWorkspaceKind, TargetKind, WorkspaceBuildScripts, +}; use rustc_hash::{FxHashMap, FxHashSet}; use stdx::thread; use tracing::{Level, span, trace}; @@ -42,7 +44,7 @@ use crate::{ main_loop::Task, mem_docs::MemDocs, op_queue::{Cause, OpQueue}, - reload, + priming_scope, reload, target_spec::{CargoTargetSpec, ProjectJsonTargetSpec, TargetSpec}, task_pool::{DeferredTaskQueue, TaskPool}, test_runner::{CargoTestHandle, CargoTestMessage}, @@ -738,6 +740,75 @@ impl GlobalState { *fetch_receiver = crossbeam_channel::after(Duration::from_millis(100)); } } + + /// Set of crates to prime: the transitive-dependency closure of every + /// local Cargo workspace `lib`/`bin` target, `rust-project.json` workspace + /// member, and detached file. Computed once when the server becomes + /// quiescent. + /// + /// Test, example, and benchmark members are deliberately excluded — they're + /// leaves, so not priming them costs no parallelism on dependency work. + /// `bin` targets are kept because they're the crate the user is most likely + /// editing. + pub(crate) fn compute_priming_scope(&self) -> Arc<[Crate]> { + let db = self.analysis_host.raw_database(); + let all = all_crates(db); + + // Map each crate-root path to its crate(s) so target roots resolve to + // `Crate` ids. The vfs read lock is held only for this build. + let root_to_crate: FxHashMap<AbsPathBuf, Vec<Crate>> = { + let vfs = self.vfs.read(); + let mut root_to_crate: FxHashMap<AbsPathBuf, Vec<Crate>> = FxHashMap::default(); + for &krate in &*all { + let root_file = krate.data(db).root_file_id; + let path = vfs.0.file_path(root_file); + let Some(path) = path.as_path() else { + continue; + }; + root_to_crate.entry(path.to_path_buf()).or_default().push(krate); + } + root_to_crate + }; + + let mut seed: FxHashSet<Crate> = FxHashSet::default(); + for workspace in self.workspaces.iter() { + match &workspace.kind { + ProjectWorkspaceKind::Cargo { cargo, .. } + | ProjectWorkspaceKind::DetachedFile { cargo: Some((cargo, ..)), .. } => { + for pkg in cargo.packages() { + if !cargo[pkg].is_local { + continue; + } + for &target in &cargo[pkg].targets { + if !matches!( + cargo[target].kind, + TargetKind::Lib { .. } | TargetKind::Bin + ) { + continue; + } + if let Some(krates) = root_to_crate.get(&*cargo[target].root) { + seed.extend(krates.iter().copied()); + } + } + } + } + ProjectWorkspaceKind::Json(project_json) => seed.extend( + project_json + .crates() + .filter(|(_, krate)| krate.is_workspace_member) + .filter_map(|(_, krate)| root_to_crate.get(&krate.root_module)) + .flat_map(|it| it.iter().copied()), + ), + ProjectWorkspaceKind::DetachedFile { file, cargo: None } => { + if let Some(krates) = root_to_crate.get(&**file) { + seed.extend(krates.iter().copied()); + } + } + } + } + + priming_scope::compute(db, seed) + } } impl Drop for GlobalState { diff --git a/crates/rust-analyzer/src/lib.rs b/crates/rust-analyzer/src/lib.rs index 9eee516c3a..48db8e38e9 100644 --- a/crates/rust-analyzer/src/lib.rs +++ b/crates/rust-analyzer/src/lib.rs @@ -38,6 +38,7 @@ mod line_index; mod main_loop; mod mem_docs; mod op_queue; +mod priming_scope; mod reload; mod target_spec; mod task_pool; diff --git a/crates/rust-analyzer/src/main_loop.rs b/crates/rust-analyzer/src/main_loop.rs index c73be92a90..56490061a7 100644 --- a/crates/rust-analyzer/src/main_loop.rs +++ b/crates/rust-analyzer/src/main_loop.rs @@ -647,14 +647,15 @@ impl GlobalState { } fn prime_caches(&mut self, cause: String) { - tracing::debug!(%cause, "will prime caches"); + let scope = self.compute_priming_scope(); + tracing::debug!(%cause, scope_size = scope.len(), "will prime caches"); let num_worker_threads = self.config.prime_caches_num_threads(); self.task_pool.handle.spawn_with_sender(ThreadIntent::Worker, { let analysis = AssertUnwindSafe(self.snapshot().analysis); move |sender| { sender.send(Task::PrimeCaches(PrimeCachesProgress::Begin)).unwrap(); - let res = analysis.parallel_prime_caches(num_worker_threads, |progress| { + let res = analysis.parallel_prime_caches(&scope, num_worker_threads, |progress| { let report = PrimeCachesProgress::Report(progress); sender.send(Task::PrimeCaches(report)).unwrap(); }); diff --git a/crates/rust-analyzer/src/priming_scope.rs b/crates/rust-analyzer/src/priming_scope.rs new file mode 100644 index 0000000000..f3d9d89bd2 --- /dev/null +++ b/crates/rust-analyzer/src/priming_scope.rs @@ -0,0 +1,166 @@ +//! Scope computation for cache priming. + +use ide_db::{RootDatabase, base_db::Crate}; +use rustc_hash::FxHashSet; +use triomphe::Arc; + +/// Close `seeds` under transitive dependencies. +pub(crate) fn compute(db: &RootDatabase, seeds: impl IntoIterator<Item = Crate>) -> Arc<[Crate]> { + let mut closure: FxHashSet<Crate> = FxHashSet::default(); + let mut worklist: Vec<Crate> = seeds.into_iter().collect(); + while let Some(krate) = worklist.pop() { + if !closure.insert(krate) { + continue; + } + worklist.extend(krate.data(db).dependencies.iter().map(|dep| dep.crate_id)); + } + closure.into_iter().collect() +} + +#[cfg(test)] +mod tests { + use ide_db::{ + RootDatabase, + base_db::{ + CrateGraphBuilder, CrateName, CrateOrigin, CrateWorkspaceData, CratesIdMap, + DependencyBuilder, Env, LangCrateOrigin, + }, + span::{Edition, FileId}, + }; + use rustc_hash::FxHashMap; + use triomphe::Arc as TriompheArc; + use vfs::AbsPathBuf; + + use super::*; + + fn empty_ws_data() -> TriompheArc<CrateWorkspaceData> { + TriompheArc::new(CrateWorkspaceData { target: Err("".into()), toolchain: None }) + } + + /// Builds a synthetic crate graph in a fresh `RootDatabase` and returns + /// the resolved `Crate` IDs keyed by the symbolic names used in `crates`. + /// + /// `crates` is a list of `(name, origin, deps)`. The crate root file id + /// is derived from the crate's index. Dependencies must refer to crates + /// declared earlier in the list. + fn build(crates: &[(&str, CrateOrigin, &[&str])]) -> (RootDatabase, FxHashMap<String, Crate>) { + let mut db = RootDatabase::default(); + let mut graph = CrateGraphBuilder::default(); + let proc_macro_cwd = + TriompheArc::new(AbsPathBuf::assert_utf8(std::env::current_dir().unwrap())); + + let mut ids = Vec::with_capacity(crates.len()); + for (i, (name, origin, _)) in crates.iter().enumerate() { + let id = graph.add_crate_root( + FileId::from_raw((i + 1) as u32), + Edition::Edition2021, + None, + None, + Default::default(), + Default::default(), + Env::default(), + origin.clone(), + Vec::new(), + false, + proc_macro_cwd.clone(), + empty_ws_data(), + ); + ids.push((name.to_string(), id)); + } + + for (i, (_, _, deps)) in crates.iter().enumerate() { + for dep_name in *deps { + let from = ids[i].1; + let to = ids + .iter() + .find(|(n, _)| n == dep_name) + .unwrap_or_else(|| panic!("unknown dep `{dep_name}`")) + .1; + graph + .add_dep(from, DependencyBuilder::new(CrateName::new(dep_name).unwrap(), to)) + .unwrap(); + } + } + + let resolved: CratesIdMap = graph.set_in_db(&mut db); + let by_name = ids.into_iter().map(|(n, id)| (n, resolved[&id])).collect(); + (db, by_name) + } + + fn lang(name: &str) -> CrateOrigin { + CrateOrigin::Lang(LangCrateOrigin::from(name)) + } + + fn local() -> CrateOrigin { + CrateOrigin::Local { repo: None, name: None } + } + + fn library(name: &str) -> CrateOrigin { + CrateOrigin::Library { repo: None, name: CrateName::new(name).unwrap().symbol().clone() } + } + + fn primed(scope: &Arc<[Crate]>, by_name: &FxHashMap<String, Crate>) -> Vec<String> { + let lookup: FxHashMap<Crate, &str> = + by_name.iter().map(|(n, c)| (*c, n.as_str())).collect(); + let mut names: Vec<String> = scope.iter().map(|c| lookup[c].to_owned()).collect(); + names.sort(); + names + } + + #[test] + fn closes_seeds_under_deps() { + // core ← lib_a ← lib_b ← bin_c + // ↖ test_d + let (db, by) = build(&[ + ("core", lang("core"), &[]), + ("lib_a", local(), &["core"]), + ("lib_b", local(), &["lib_a"]), + ("bin_c", local(), &["lib_b"]), + ("test_d", local(), &["lib_b"]), + ]); + + let scope = compute(&db, [by["lib_a"], by["lib_b"]]); + assert_eq!(primed(&scope, &by), vec!["core", "lib_a", "lib_b"]); + } + + #[test] + fn active_seed_pulls_transitive_deps() { + let (db, by) = build(&[ + ("core", lang("core"), &[]), + ("lib_a", local(), &["core"]), + ("lib_b", local(), &["lib_a"]), + ("bin_c", local(), &["lib_b"]), + ]); + + let scope = compute(&db, [by["bin_c"]]); + assert_eq!(primed(&scope, &by), vec!["bin_c", "core", "lib_a", "lib_b"]); + } + + #[test] + fn does_not_pull_reverse_deps() { + // Seeding only the leaf must not warm crates that depend on it. + let (db, by) = build(&[ + ("core", lang("core"), &[]), + ("lib_leaf", local(), &["core"]), + ("dependent_a", local(), &["lib_leaf"]), + ("dependent_b", local(), &["lib_leaf"]), + ]); + + let scope = compute(&db, [by["lib_leaf"]]); + assert_eq!(primed(&scope, &by), vec!["core", "lib_leaf"]); + } + + #[test] + fn pulls_library_dep_via_seeded_crate() { + // A `Library` (non-workspace-member) origin behaves like any other — + // dep walk is origin-agnostic, only the seed selection cares. + let (db, by) = build(&[ + ("core", lang("core"), &[]), + ("ext", library("serde"), &["core"]), + ("lib_a", local(), &["ext"]), + ]); + + let scope = compute(&db, [by["lib_a"]]); + assert_eq!(primed(&scope, &by), vec!["core", "ext", "lib_a"]); + } +} |