Skip to content

Debounce workspace fetching for workspace structure changes #19814

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 38 additions & 6 deletions crates/rust-analyzer/src/global_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
//!
//! Each tick provides an immutable snapshot of the state as `WorldSnapshot`.

use std::{ops::Not as _, panic::AssertUnwindSafe, time::Instant};
use std::{
ops::Not as _,
panic::AssertUnwindSafe,
time::{Duration, Instant},
};

use crossbeam_channel::{Receiver, Sender, unbounded};
use hir::ChangeWithProcMacros;
Expand Down Expand Up @@ -41,6 +45,7 @@ use crate::{
test_runner::{CargoTestHandle, CargoTestMessage},
};

#[derive(Debug)]
pub(crate) struct FetchWorkspaceRequest {
pub(crate) path: Option<AbsPathBuf>,
pub(crate) force_crate_graph_reload: bool,
Expand Down Expand Up @@ -116,6 +121,11 @@ pub(crate) struct GlobalState {
pub(crate) discover_sender: Sender<discover::DiscoverProjectMessage>,
pub(crate) discover_receiver: Receiver<discover::DiscoverProjectMessage>,

// Debouncing channel for fetching the workspace
// we want to delay it until the VFS looks stable-ish (and thus is not currently in the middle
// of a VCS operation like `git switch`)
pub(crate) fetch_ws_receiver: Option<(Receiver<Instant>, FetchWorkspaceRequest)>,

// VFS
pub(crate) loader: Handle<Box<dyn vfs::loader::Handle>, Receiver<vfs::loader::Message>>,
pub(crate) vfs: Arc<RwLock<(vfs::Vfs, FxHashMap<FileId, LineEndings>)>>,
Expand Down Expand Up @@ -268,6 +278,8 @@ impl GlobalState {
discover_sender,
discover_receiver,

fetch_ws_receiver: None,

vfs: Arc::new(RwLock::new((vfs::Vfs::default(), Default::default()))),
vfs_config_version: 0,
vfs_progress_config_version: 0,
Expand Down Expand Up @@ -519,11 +531,7 @@ impl GlobalState {
if let Some((path, force_crate_graph_reload)) = workspace_structure_change {
let _p = span!(Level::INFO, "GlobalState::process_changes/ws_structure_change")
.entered();

self.fetch_workspaces_queue.request_op(
format!("workspace vfs file change: {path}"),
FetchWorkspaceRequest { path: Some(path), force_crate_graph_reload },
);
self.enqueue_workspace_fetch(path, force_crate_graph_reload);
}
}

Expand Down Expand Up @@ -671,6 +679,30 @@ impl GlobalState {
None
})
}

fn enqueue_workspace_fetch(&mut self, path: AbsPathBuf, force_crate_graph_reload: bool) {
let already_requested = self.fetch_workspaces_queue.op_requested()
&& !self.fetch_workspaces_queue.op_in_progress();
if self.fetch_ws_receiver.is_none() && already_requested {
// Don't queue up a new fetch request if we already have done so
// Otherwise we will re-fetch in quick succession which is unnecessary
// Note though, that if one is already in progress, we *want* to re-queue
// as the in-progress fetch might not have the latest changes in it anymore
// FIXME: We should cancel the in-progress fetch here
return;
}

self.fetch_ws_receiver = Some((
crossbeam_channel::after(Duration::from_millis(100)),
FetchWorkspaceRequest { path: Some(path), force_crate_graph_reload },
));
}

pub(crate) fn debounce_workspace_fetch(&mut self) {
if let Some((fetch_receiver, _)) = &mut self.fetch_ws_receiver {
*fetch_receiver = crossbeam_channel::after(Duration::from_millis(100));
}
}
}

impl Drop for GlobalState {
Expand Down
16 changes: 14 additions & 2 deletions crates/rust-analyzer/src/main_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use std::{
time::{Duration, Instant},
};

use crossbeam_channel::{Receiver, select};
use crossbeam_channel::{Receiver, never, select};
use ide_db::base_db::{SourceDatabase, VfsPath, salsa::Database as _};
use lsp_server::{Connection, Notification, Request};
use lsp_types::{TextDocumentIdentifier, notification::Notification as _};
Expand Down Expand Up @@ -71,6 +71,7 @@ enum Event {
Flycheck(FlycheckMessage),
TestResult(CargoTestMessage),
DiscoverProject(DiscoverProjectMessage),
FetchWorkspaces(FetchWorkspaceRequest),
}

impl fmt::Display for Event {
Expand All @@ -83,6 +84,7 @@ impl fmt::Display for Event {
Event::QueuedTask(_) => write!(f, "Event::QueuedTask"),
Event::TestResult(_) => write!(f, "Event::TestResult"),
Event::DiscoverProject(_) => write!(f, "Event::DiscoverProject"),
Event::FetchWorkspaces(_) => write!(f, "Event::SwitchWorkspaces"),
}
}
}
Expand Down Expand Up @@ -150,6 +152,7 @@ impl fmt::Debug for Event {
}
_ => (),
}

match self {
Event::Lsp(it) => fmt::Debug::fmt(it, f),
Event::Task(it) => fmt::Debug::fmt(it, f),
Expand All @@ -158,6 +161,7 @@ impl fmt::Debug for Event {
Event::Flycheck(it) => fmt::Debug::fmt(it, f),
Event::TestResult(it) => fmt::Debug::fmt(it, f),
Event::DiscoverProject(it) => fmt::Debug::fmt(it, f),
Event::FetchWorkspaces(it) => fmt::Debug::fmt(it, f),
}
}
}
Expand Down Expand Up @@ -251,7 +255,7 @@ impl GlobalState {
}

fn next_event(
&self,
&mut self,
inbox: &Receiver<lsp_server::Message>,
) -> Result<Option<Event>, crossbeam_channel::RecvError> {
// Make sure we reply to formatting requests ASAP so the editor doesn't block
Expand Down Expand Up @@ -283,6 +287,10 @@ impl GlobalState {

recv(self.discover_receiver) -> task =>
task.map(Event::DiscoverProject),

recv(self.fetch_ws_receiver.as_ref().map_or(&never(), |(chan, _)| chan)) -> _instant => {
Ok(Event::FetchWorkspaces(self.fetch_ws_receiver.take().unwrap().1))
},
}
.map(Some)
}
Expand Down Expand Up @@ -412,6 +420,9 @@ impl GlobalState {
self.handle_discover_msg(message);
}
}
Event::FetchWorkspaces(req) => {
self.fetch_workspaces_queue.request_op("project structure change".to_owned(), req)
}
}
let event_handling_duration = loop_start.elapsed();
let (state_changed, memdocs_added_or_removed) = if self.vfs_done {
Expand Down Expand Up @@ -830,6 +841,7 @@ impl GlobalState {
match message {
vfs::loader::Message::Changed { files } | vfs::loader::Message::Loaded { files } => {
let _p = tracing::info_span!("GlobalState::handle_vfs_msg{changed/load}").entered();
self.debounce_workspace_fetch();
let vfs = &mut self.vfs.write().0;
for (path, contents) in files {
let path = VfsPath::from(path);
Expand Down
2 changes: 1 addition & 1 deletion crates/rust-analyzer/src/op_queue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ impl<Args, Output> Default for OpQueue<Args, Output> {
}
}

impl<Args, Output> OpQueue<Args, Output> {
impl<Args: std::fmt::Debug, Output> OpQueue<Args, Output> {
/// Request an operation to start.
pub(crate) fn request_op(&mut self, reason: Cause, args: Args) {
self.op_requested = Some((reason, args));
Expand Down
1 change: 1 addition & 0 deletions crates/rust-analyzer/src/reload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ impl GlobalState {
/// are ready to do semantic work.
pub(crate) fn is_quiescent(&self) -> bool {
self.vfs_done
&& self.fetch_ws_receiver.is_none()
&& !self.fetch_workspaces_queue.op_in_progress()
&& !self.fetch_build_data_queue.op_in_progress()
&& !self.fetch_proc_macros_queue.op_in_progress()
Expand Down