Skip to content

Commit 28f64df

Browse files
committed
perf: Request cancellation while processing changed files
1 parent d7e977a commit 28f64df

File tree

4 files changed

+137
-80
lines changed

4 files changed

+137
-80
lines changed

Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/rust-analyzer/src/global_state.rs

+88-76
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
//!
44
//! Each tick provides an immutable snapshot of the state as `WorldSnapshot`.
55
6-
use std::{ops::Not as _, time::Instant};
6+
use std::{ops::Not as _, panic::AssertUnwindSafe, time::Instant};
77

88
use crossbeam_channel::{Receiver, Sender, unbounded};
99
use hir::ChangeWithProcMacros;
@@ -19,6 +19,7 @@ use parking_lot::{
1919
use proc_macro_api::ProcMacroClient;
2020
use project_model::{ManifestPath, ProjectWorkspace, ProjectWorkspaceKind, WorkspaceBuildScripts};
2121
use rustc_hash::{FxHashMap, FxHashSet};
22+
use stdx::thread;
2223
use tracing::{Level, span, trace};
2324
use triomphe::Arc;
2425
use vfs::{AbsPathBuf, AnchoredPathBuf, ChangeKind, Vfs, VfsPath};
@@ -78,6 +79,7 @@ pub(crate) struct GlobalState {
7879

7980
pub(crate) task_pool: Handle<TaskPool<Task>, Receiver<Task>>,
8081
pub(crate) fmt_pool: Handle<TaskPool<Task>, Receiver<Task>>,
82+
pub(crate) cancellation_pool: thread::Pool,
8183

8284
pub(crate) config: Arc<Config>,
8385
pub(crate) config_errors: Option<ConfigErrors>,
@@ -210,6 +212,7 @@ impl GlobalState {
210212
let handle = TaskPool::new_with_threads(sender, 1);
211213
Handle { handle, receiver }
212214
};
215+
let cancellation_pool = thread::Pool::new(1);
213216

214217
let task_queue = {
215218
let (sender, receiver) = unbounded();
@@ -230,6 +233,7 @@ impl GlobalState {
230233
req_queue: ReqQueue::default(),
231234
task_pool,
232235
fmt_pool,
236+
cancellation_pool,
233237
loader,
234238
config: Arc::new(config.clone()),
235239
analysis_host,
@@ -290,74 +294,83 @@ impl GlobalState {
290294

291295
pub(crate) fn process_changes(&mut self) -> bool {
292296
let _p = span!(Level::INFO, "GlobalState::process_changes").entered();
293-
294297
// We cannot directly resolve a change in a ratoml file to a format
295298
// that can be used by the config module because config talks
296299
// in `SourceRootId`s instead of `FileId`s and `FileId` -> `SourceRootId`
297300
// mapping is not ready until `AnalysisHost::apply_changes` has been called.
298301
let mut modified_ratoml_files: FxHashMap<FileId, (ChangeKind, vfs::VfsPath)> =
299302
FxHashMap::default();
300303

301-
let (change, modified_rust_files, workspace_structure_change) = {
302-
let mut change = ChangeWithProcMacros::default();
303-
let mut guard = self.vfs.write();
304-
let changed_files = guard.0.take_changes();
305-
if changed_files.is_empty() {
306-
return false;
307-
}
304+
let mut change = ChangeWithProcMacros::default();
305+
let mut guard = self.vfs.write();
306+
let changed_files = guard.0.take_changes();
307+
if changed_files.is_empty() {
308+
return false;
309+
}
308310

309-
// downgrade to read lock to allow more readers while we are normalizing text
310-
let guard = RwLockWriteGuard::downgrade_to_upgradable(guard);
311-
let vfs: &Vfs = &guard.0;
312-
313-
let mut workspace_structure_change = None;
314-
// A file was added or deleted
315-
let mut has_structure_changes = false;
316-
let mut bytes = vec![];
317-
let mut modified_rust_files = vec![];
318-
for file in changed_files.into_values() {
319-
let vfs_path = vfs.file_path(file.file_id);
320-
if let Some(("rust-analyzer", Some("toml"))) = vfs_path.name_and_extension() {
321-
// Remember ids to use them after `apply_changes`
322-
modified_ratoml_files.insert(file.file_id, (file.kind(), vfs_path.clone()));
323-
}
311+
let (change, modified_rust_files, workspace_structure_change) =
312+
self.cancellation_pool.scoped(|s| {
313+
// start cancellation in parallel, this will kick off lru eviction
314+
// allowing us to do meaningful work while waiting
315+
// FIXME: We should have a long living thread for this purpose instead of re-spawning.
316+
let analysis_host = AssertUnwindSafe(&mut self.analysis_host);
317+
s.spawn(thread::ThreadIntent::LatencySensitive, || {
318+
{ analysis_host }.0.request_cancellation()
319+
});
320+
321+
// downgrade to read lock to allow more readers while we are normalizing text
322+
let guard = RwLockWriteGuard::downgrade_to_upgradable(guard);
323+
let vfs: &Vfs = &guard.0;
324+
325+
let mut workspace_structure_change = None;
326+
// A file was added or deleted
327+
let mut has_structure_changes = false;
328+
let mut bytes = vec![];
329+
let mut modified_rust_files = vec![];
330+
for file in changed_files.into_values() {
331+
let vfs_path = vfs.file_path(file.file_id);
332+
if let Some(("rust-analyzer", Some("toml"))) = vfs_path.name_and_extension() {
333+
// Remember ids to use them after `apply_changes`
334+
modified_ratoml_files.insert(file.file_id, (file.kind(), vfs_path.clone()));
335+
}
324336

325-
if let Some(path) = vfs_path.as_path() {
326-
has_structure_changes |= file.is_created_or_deleted();
337+
if let Some(path) = vfs_path.as_path() {
338+
has_structure_changes |= file.is_created_or_deleted();
327339

328-
if file.is_modified() && path.extension() == Some("rs") {
329-
modified_rust_files.push(file.file_id);
330-
}
340+
if file.is_modified() && path.extension() == Some("rs") {
341+
modified_rust_files.push(file.file_id);
342+
}
331343

332-
let additional_files = self
333-
.config
334-
.discover_workspace_config()
335-
.map(|cfg| {
336-
cfg.files_to_watch.iter().map(String::as_str).collect::<Vec<&str>>()
337-
})
338-
.unwrap_or_default();
339-
340-
let path = path.to_path_buf();
341-
if file.is_created_or_deleted() {
342-
workspace_structure_change.get_or_insert((path, false)).1 |=
343-
self.crate_graph_file_dependencies.contains(vfs_path);
344-
} else if reload::should_refresh_for_change(
345-
&path,
346-
file.kind(),
347-
&additional_files,
348-
) {
349-
trace!(?path, kind = ?file.kind(), "refreshing for a change");
350-
workspace_structure_change.get_or_insert((path.clone(), false));
344+
let additional_files = self
345+
.config
346+
.discover_workspace_config()
347+
.map(|cfg| {
348+
cfg.files_to_watch.iter().map(String::as_str).collect::<Vec<&str>>()
349+
})
350+
.unwrap_or_default();
351+
352+
let path = path.to_path_buf();
353+
if file.is_created_or_deleted() {
354+
workspace_structure_change.get_or_insert((path, false)).1 |=
355+
self.crate_graph_file_dependencies.contains(vfs_path);
356+
} else if reload::should_refresh_for_change(
357+
&path,
358+
file.kind(),
359+
&additional_files,
360+
) {
361+
trace!(?path, kind = ?file.kind(), "refreshing for a change");
362+
workspace_structure_change.get_or_insert((path.clone(), false));
363+
}
351364
}
352-
}
353365

354-
// Clear native diagnostics when their file gets deleted
355-
if !file.exists() {
356-
self.diagnostics.clear_native_for(file.file_id);
357-
}
366+
// Clear native diagnostics when their file gets deleted
367+
if !file.exists() {
368+
self.diagnostics.clear_native_for(file.file_id);
369+
}
358370

359-
let text =
360-
if let vfs::Change::Create(v, _) | vfs::Change::Modify(v, _) = file.change {
371+
let text = if let vfs::Change::Create(v, _) | vfs::Change::Modify(v, _) =
372+
file.change
373+
{
361374
String::from_utf8(v).ok().map(|text| {
362375
// FIXME: Consider doing normalization in the `vfs` instead? That allows
363376
// getting rid of some locking
@@ -367,29 +380,28 @@ impl GlobalState {
367380
} else {
368381
None
369382
};
370-
// delay `line_endings_map` changes until we are done normalizing the text
371-
// this allows delaying the re-acquisition of the write lock
372-
bytes.push((file.file_id, text));
373-
}
374-
let (vfs, line_endings_map) = &mut *RwLockUpgradableReadGuard::upgrade(guard);
375-
bytes.into_iter().for_each(|(file_id, text)| {
376-
let text = match text {
377-
None => None,
378-
Some((text, line_endings)) => {
379-
line_endings_map.insert(file_id, line_endings);
380-
Some(text)
381-
}
382-
};
383-
change.change_file(file_id, text);
383+
// delay `line_endings_map` changes until we are done normalizing the text
384+
// this allows delaying the re-acquisition of the write lock
385+
bytes.push((file.file_id, text));
386+
}
387+
let (vfs, line_endings_map) = &mut *RwLockUpgradableReadGuard::upgrade(guard);
388+
bytes.into_iter().for_each(|(file_id, text)| {
389+
let text = match text {
390+
None => None,
391+
Some((text, line_endings)) => {
392+
line_endings_map.insert(file_id, line_endings);
393+
Some(text)
394+
}
395+
};
396+
change.change_file(file_id, text);
397+
});
398+
if has_structure_changes {
399+
let roots = self.source_root_config.partition(vfs);
400+
change.set_roots(roots);
401+
}
402+
(change, modified_rust_files, workspace_structure_change)
384403
});
385-
if has_structure_changes {
386-
let roots = self.source_root_config.partition(vfs);
387-
change.set_roots(roots);
388-
}
389-
(change, modified_rust_files, workspace_structure_change)
390-
};
391404

392-
let _p = span!(Level::INFO, "GlobalState::process_changes/apply_change").entered();
393405
self.analysis_host.apply_change(change);
394406
if !modified_ratoml_files.is_empty()
395407
|| !self.config.same_source_root_parent_map(&self.local_roots_parent_map)

crates/stdx/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ jod-thread = "1.0.0"
1717
crossbeam-channel.workspace = true
1818
itertools.workspace = true
1919
tracing.workspace = true
20+
crossbeam-utils = "0.8.21"
2021
# Think twice before adding anything here
2122

2223
[target.'cfg(unix)'.dependencies]

crates/stdx/src/thread/pool.rs

+47-4
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
//! the threading utilities in [`crate::thread`].
99
1010
use std::{
11+
marker::PhantomData,
1112
panic::{self, UnwindSafe},
1213
sync::{
1314
Arc,
@@ -16,8 +17,9 @@ use std::{
1617
};
1718

1819
use crossbeam_channel::{Receiver, Sender};
20+
use crossbeam_utils::sync::WaitGroup;
1921

20-
use super::{Builder, JoinHandle, ThreadIntent};
22+
use crate::thread::{Builder, JoinHandle, ThreadIntent};
2123

2224
pub struct Pool {
2325
// `_handles` is never read: the field is present
@@ -79,9 +81,6 @@ impl Pool {
7981
Self { _handles: handles.into_boxed_slice(), extant_tasks, job_sender }
8082
}
8183

82-
/// # Panics
83-
///
84-
/// Panics if job panics
8584
pub fn spawn<F>(&self, intent: ThreadIntent, f: F)
8685
where
8786
F: FnOnce() + Send + UnwindSafe + 'static,
@@ -97,6 +96,17 @@ impl Pool {
9796
self.job_sender.send(job).unwrap();
9897
}
9998

99+
pub fn scoped<'pool, 'scope, F, R>(&'pool self, f: F) -> R
100+
where
101+
F: FnOnce(&Scope<'pool, 'scope>) -> R,
102+
{
103+
let wg = WaitGroup::new();
104+
let scope = Scope { pool: self, wg, _marker: PhantomData };
105+
let r = f(&scope);
106+
scope.wg.wait();
107+
r
108+
}
109+
100110
#[must_use]
101111
pub fn len(&self) -> usize {
102112
self.extant_tasks.load(Ordering::SeqCst)
@@ -107,3 +117,36 @@ impl Pool {
107117
self.len() == 0
108118
}
109119
}
120+
121+
pub struct Scope<'pool, 'scope> {
122+
pool: &'pool Pool,
123+
wg: WaitGroup,
124+
_marker: PhantomData<fn(&'scope ()) -> &'scope ()>,
125+
}
126+
127+
impl<'pool, 'scope> Scope<'pool, 'scope> {
128+
pub fn spawn<F>(&self, intent: ThreadIntent, f: F)
129+
where
130+
F: 'scope + FnOnce() + Send + UnwindSafe,
131+
{
132+
let wg = self.wg.clone();
133+
let f = Box::new(move || {
134+
if cfg!(debug_assertions) {
135+
intent.assert_is_used_on_current_thread();
136+
}
137+
f();
138+
drop(wg);
139+
});
140+
141+
let job = Job {
142+
requested_intent: intent,
143+
f: unsafe {
144+
std::mem::transmute::<
145+
Box<dyn 'scope + FnOnce() + Send + UnwindSafe>,
146+
Box<dyn 'static + FnOnce() + Send + UnwindSafe>,
147+
>(f)
148+
},
149+
};
150+
self.pool.job_sender.send(job).unwrap();
151+
}
152+
}

0 commit comments

Comments
 (0)