Unnamed repository; edit this file 'description' to name the repository.
Merge pull request #22587 from tilladam/perf/scoped-lazy-priming
Scoped lazy priming
Lukas Wirth 2 weeks ago
parent 1d513f5 · parent 25838b4 · commit a121446
-rw-r--r--crates/ide-db/src/prime_caches.rs42
-rw-r--r--crates/ide/src/lib.rs14
-rw-r--r--crates/ide/src/syntax_highlighting/tests.rs2
-rw-r--r--crates/load-cargo/src/lib.rs3
-rw-r--r--crates/project-model/src/project_json.rs2
-rw-r--r--crates/rust-analyzer/src/cli/prime_caches.rs3
-rw-r--r--crates/rust-analyzer/src/global_state.rs77
-rw-r--r--crates/rust-analyzer/src/lib.rs1
-rw-r--r--crates/rust-analyzer/src/main_loop.rs5
-rw-r--r--crates/rust-analyzer/src/priming_scope.rs166
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"]);
+ }
+}