Emit less update events for odd FS events (#39557)
When running flycheck, I've noticed that scrolling starts to lag: https://github.com/user-attachments/assets/b0bef0a3-ccbd-479d-a385-273398086d38 When checking the trace, it is notable that project panel updates its entire tree multiple times during flycheck: <img width="2032" height="1136" alt="image" src="https://github.com/user-attachments/assets/d1935e77-3b00-4be5-a12a-8a17a9d64202" /> [scrolling.trace.zip](https://github.com/user-attachments/files/22710852/scrolling.trace.zip) Turns out, `target/debug` directory is loaded by Zed (presumably, reported by langserver as there are sources generated by bindgen and proto that need to be loaded), and `target/debug/build` directory received multiple events of a `None` kind for Zed, which trigger the rescans. Rework the logic to omit the `None`-kind events in Zed, and to avoid excessive repo updates if not needed. Release Notes: - Improved worktree FS event emits in gitignored directories --------- Co-authored-by: Cole Miller <cole@zed.dev>
This commit is contained in:
co-authored by
Cole Miller
parent
46b6adadf9
commit
469ecfbe13
@@ -298,7 +298,7 @@ pub enum RepositoryState {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum RepositoryEvent {
|
||||
Updated { full_scan: bool, new_instance: bool },
|
||||
MergeHeadsChanged,
|
||||
@@ -4839,17 +4839,20 @@ impl Repository {
|
||||
this.snapshot.scan_id += 1;
|
||||
}
|
||||
|
||||
if needs_update && let Some(updates_tx) = updates_tx {
|
||||
updates_tx
|
||||
.unbounded_send(DownstreamUpdate::UpdateRepository(
|
||||
this.snapshot.clone(),
|
||||
))
|
||||
.ok();
|
||||
if needs_update {
|
||||
if let Some(updates_tx) = updates_tx {
|
||||
updates_tx
|
||||
.unbounded_send(DownstreamUpdate::UpdateRepository(
|
||||
this.snapshot.clone(),
|
||||
))
|
||||
.ok();
|
||||
}
|
||||
|
||||
cx.emit(RepositoryEvent::Updated {
|
||||
full_scan: false,
|
||||
new_instance: false,
|
||||
});
|
||||
}
|
||||
cx.emit(RepositoryEvent::Updated {
|
||||
full_scan: false,
|
||||
new_instance: false,
|
||||
});
|
||||
})
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
#![allow(clippy::format_collect)]
|
||||
|
||||
use crate::{
|
||||
Event, git_store::StatusEntry, task_inventory::TaskContexts, task_store::TaskSettingsLocation,
|
||||
Event,
|
||||
git_store::{GitStoreEvent, RepositoryEvent, StatusEntry},
|
||||
task_inventory::TaskContexts,
|
||||
task_store::TaskSettingsLocation,
|
||||
*,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
@@ -39,7 +42,14 @@ use rand::{Rng as _, rngs::StdRng};
|
||||
use serde_json::json;
|
||||
#[cfg(not(windows))]
|
||||
use std::os;
|
||||
use std::{env, mem, num::NonZeroU32, ops::Range, str::FromStr, sync::OnceLock, task::Poll};
|
||||
use std::{
|
||||
env, mem,
|
||||
num::NonZeroU32,
|
||||
ops::Range,
|
||||
str::FromStr,
|
||||
sync::{Arc, OnceLock},
|
||||
task::Poll,
|
||||
};
|
||||
use task::{ResolvedTask, ShellKind, TaskContext};
|
||||
use unindent::Unindent as _;
|
||||
use util::{
|
||||
@@ -8830,7 +8840,297 @@ async fn test_file_status(cx: &mut gpui::TestAppContext) {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
#[cfg_attr(target_os = "windows", ignore)]
|
||||
async fn test_ignored_dirs_events(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
cx.executor().allow_parking();
|
||||
|
||||
const IGNORE_RULE: &str = "**/target";
|
||||
|
||||
let root = TempTree::new(json!({
|
||||
"project": {
|
||||
"src": {
|
||||
"main.rs": "fn main() {}"
|
||||
},
|
||||
"target": {
|
||||
"debug": {
|
||||
"important_text.txt": "important text",
|
||||
},
|
||||
},
|
||||
".gitignore": IGNORE_RULE
|
||||
},
|
||||
|
||||
}));
|
||||
let root_path = root.path();
|
||||
|
||||
// Set up git repository before creating the worktree.
|
||||
let work_dir = root.path().join("project");
|
||||
let repo = git_init(work_dir.as_path());
|
||||
repo.add_ignore_rule(IGNORE_RULE).unwrap();
|
||||
git_add("src/main.rs", &repo);
|
||||
git_add(".gitignore", &repo);
|
||||
git_commit("Initial commit", &repo);
|
||||
|
||||
let project = Project::test(Arc::new(RealFs::new(None, cx.executor())), [root_path], cx).await;
|
||||
let repository_updates = Arc::new(Mutex::new(Vec::new()));
|
||||
let project_events = Arc::new(Mutex::new(Vec::new()));
|
||||
project.update(cx, |project, cx| {
|
||||
let repo_events = repository_updates.clone();
|
||||
cx.subscribe(project.git_store(), move |_, _, e, _| {
|
||||
if let GitStoreEvent::RepositoryUpdated(_, e, _) = e {
|
||||
repo_events.lock().push(e.clone());
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
let project_events = project_events.clone();
|
||||
cx.subscribe_self(move |_, e, _| {
|
||||
if let Event::WorktreeUpdatedEntries(_, updates) = e {
|
||||
project_events.lock().extend(
|
||||
updates
|
||||
.iter()
|
||||
.map(|(path, _, change)| (path.as_unix_str().to_string(), *change))
|
||||
.filter(|(path, _)| path != "fs-event-sentinel"),
|
||||
);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
|
||||
let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
|
||||
tree.flush_fs_events(cx).await;
|
||||
tree.update(cx, |tree, cx| {
|
||||
tree.load_file(rel_path("project/target/debug/important_text.txt"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
tree.update(cx, |tree, _| {
|
||||
assert_eq!(
|
||||
tree.entries(true, 0)
|
||||
.map(|entry| (entry.path.as_ref(), entry.is_ignored))
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
(rel_path(""), false),
|
||||
(rel_path("project/"), false),
|
||||
(rel_path("project/.gitignore"), false),
|
||||
(rel_path("project/src"), false),
|
||||
(rel_path("project/src/main.rs"), false),
|
||||
(rel_path("project/target"), true),
|
||||
(rel_path("project/target/debug"), true),
|
||||
(rel_path("project/target/debug/important_text.txt"), true),
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
repository_updates.lock().drain(..).collect::<Vec<_>>(),
|
||||
vec![
|
||||
RepositoryEvent::Updated {
|
||||
full_scan: true,
|
||||
new_instance: false,
|
||||
},
|
||||
RepositoryEvent::MergeHeadsChanged,
|
||||
],
|
||||
"Initial worktree scan should produce a repo update event"
|
||||
);
|
||||
assert_eq!(
|
||||
project_events.lock().drain(..).collect::<Vec<_>>(),
|
||||
vec![
|
||||
("project/target".to_string(), PathChange::Loaded),
|
||||
("project/target/debug".to_string(), PathChange::Loaded),
|
||||
(
|
||||
"project/target/debug/important_text.txt".to_string(),
|
||||
PathChange::Loaded
|
||||
),
|
||||
],
|
||||
"Initial project changes should show that all not-ignored and all opened files are loaded"
|
||||
);
|
||||
|
||||
let deps_dir = work_dir.join("target").join("debug").join("deps");
|
||||
std::fs::create_dir_all(&deps_dir).unwrap();
|
||||
tree.flush_fs_events(cx).await;
|
||||
project
|
||||
.update(cx, |project, cx| project.git_scans_complete(cx))
|
||||
.await;
|
||||
cx.executor().run_until_parked();
|
||||
std::fs::write(deps_dir.join("aa.tmp"), "something tmp").unwrap();
|
||||
tree.flush_fs_events(cx).await;
|
||||
project
|
||||
.update(cx, |project, cx| project.git_scans_complete(cx))
|
||||
.await;
|
||||
cx.executor().run_until_parked();
|
||||
std::fs::remove_dir_all(&deps_dir).unwrap();
|
||||
tree.flush_fs_events(cx).await;
|
||||
project
|
||||
.update(cx, |project, cx| project.git_scans_complete(cx))
|
||||
.await;
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
tree.update(cx, |tree, _| {
|
||||
assert_eq!(
|
||||
tree.entries(true, 0)
|
||||
.map(|entry| (entry.path.as_ref(), entry.is_ignored))
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
(rel_path(""), false),
|
||||
(rel_path("project/"), false),
|
||||
(rel_path("project/.gitignore"), false),
|
||||
(rel_path("project/src"), false),
|
||||
(rel_path("project/src/main.rs"), false),
|
||||
(rel_path("project/target"), true),
|
||||
(rel_path("project/target/debug"), true),
|
||||
(rel_path("project/target/debug/important_text.txt"), true),
|
||||
],
|
||||
"No stray temp files should be left after the flycheck changes"
|
||||
);
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
repository_updates.lock().as_slice(),
|
||||
Vec::new(),
|
||||
"No further repo events should happen, as only ignored dirs' contents was changed",
|
||||
);
|
||||
assert_eq!(
|
||||
project_events.lock().as_slice(),
|
||||
vec![
|
||||
("project/target/debug/deps".to_string(), PathChange::Added),
|
||||
("project/target/debug/deps".to_string(), PathChange::Removed),
|
||||
],
|
||||
"Due to `debug` directory being tracket, it should get updates for entries inside it.
|
||||
No updates for more nested directories should happen as those are ignored",
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_odd_events_for_ignored_dirs(
|
||||
executor: BackgroundExecutor,
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(executor);
|
||||
fs.insert_tree(
|
||||
path!("/root"),
|
||||
json!({
|
||||
".git": {},
|
||||
".gitignore": "**/target/",
|
||||
"src": {
|
||||
"main.rs": "fn main() {}",
|
||||
},
|
||||
"target": {
|
||||
"debug": {
|
||||
"foo.txt": "foo",
|
||||
"deps": {}
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
fs.set_head_and_index_for_repo(
|
||||
path!("/root/.git").as_ref(),
|
||||
&[
|
||||
(".gitignore", "**/target/".into()),
|
||||
("src/main.rs", "fn main() {}".into()),
|
||||
],
|
||||
);
|
||||
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
let repository_updates = Arc::new(Mutex::new(Vec::new()));
|
||||
let project_events = Arc::new(Mutex::new(Vec::new()));
|
||||
project.update(cx, |project, cx| {
|
||||
let repository_updates = repository_updates.clone();
|
||||
cx.subscribe(project.git_store(), move |_, _, e, _| {
|
||||
if let GitStoreEvent::RepositoryUpdated(_, e, _) = e {
|
||||
repository_updates.lock().push(e.clone());
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
let project_events = project_events.clone();
|
||||
cx.subscribe_self(move |_, e, _| {
|
||||
if let Event::WorktreeUpdatedEntries(_, updates) = e {
|
||||
project_events.lock().extend(
|
||||
updates
|
||||
.iter()
|
||||
.map(|(path, _, change)| (path.as_unix_str().to_string(), *change))
|
||||
.filter(|(path, _)| path != "fs-event-sentinel"),
|
||||
);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
|
||||
let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
|
||||
tree.update(cx, |tree, cx| {
|
||||
tree.load_file(rel_path("target/debug/foo.txt"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
tree.flush_fs_events(cx).await;
|
||||
project
|
||||
.update(cx, |project, cx| project.git_scans_complete(cx))
|
||||
.await;
|
||||
cx.run_until_parked();
|
||||
tree.update(cx, |tree, _| {
|
||||
assert_eq!(
|
||||
tree.entries(true, 0)
|
||||
.map(|entry| (entry.path.as_ref(), entry.is_ignored))
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
(rel_path(""), false),
|
||||
(rel_path(".gitignore"), false),
|
||||
(rel_path("src"), false),
|
||||
(rel_path("src/main.rs"), false),
|
||||
(rel_path("target"), true),
|
||||
(rel_path("target/debug"), true),
|
||||
(rel_path("target/debug/deps"), true),
|
||||
(rel_path("target/debug/foo.txt"), true),
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
repository_updates.lock().drain(..).collect::<Vec<_>>(),
|
||||
vec![
|
||||
RepositoryEvent::Updated {
|
||||
full_scan: true,
|
||||
new_instance: false,
|
||||
},
|
||||
RepositoryEvent::MergeHeadsChanged,
|
||||
],
|
||||
"Initial worktree scan should produce a repo update event"
|
||||
);
|
||||
assert_eq!(
|
||||
project_events.lock().drain(..).collect::<Vec<_>>(),
|
||||
vec![
|
||||
("target".to_string(), PathChange::Loaded),
|
||||
("target/debug".to_string(), PathChange::Loaded),
|
||||
("target/debug/deps".to_string(), PathChange::Loaded),
|
||||
("target/debug/foo.txt".to_string(), PathChange::Loaded),
|
||||
],
|
||||
"All non-ignored entries and all opened firs should be getting a project event",
|
||||
);
|
||||
|
||||
// Emulate a flycheck spawn: it emits a `INODE_META_MOD`-flagged FS event on target/debug/deps, then creates and removes temp files inside.
|
||||
// This may happen multiple times during a single flycheck, but once is enough for testing.
|
||||
fs.emit_fs_event("/root/target/debug/deps", None);
|
||||
tree.flush_fs_events(cx).await;
|
||||
project
|
||||
.update(cx, |project, cx| project.git_scans_complete(cx))
|
||||
.await;
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
repository_updates.lock().as_slice(),
|
||||
Vec::new(),
|
||||
"No further repo events should happen, as only ignored dirs received FS events",
|
||||
);
|
||||
assert_eq!(
|
||||
project_events.lock().as_slice(),
|
||||
Vec::new(),
|
||||
"No further project events should happen, as only ignored dirs received FS events",
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_repos_in_invisible_worktrees(
|
||||
executor: BackgroundExecutor,
|
||||
cx: &mut gpui::TestAppContext,
|
||||
|
||||
Reference in New Issue
Block a user