Avoid holding worktree lock for a long time while updating large repos' git status (#12266)
Fixes https://github.com/zed-industries/zed/issues/9575 Fixes https://github.com/zed-industries/zed/issues/4294 ### Problem When a large git repository's `.git` folder changes (due to a `git commit`, `git reset` etc), Zed needs to recompute the git status for every file in that git repository. Part of computing the git status is the *unstaged* part - the comparison between the content of the file and the version in the git index. In a large git repository like `chromium` or `linux`, this is inherently pretty slow. Previously, we performed this git status all at once, and held a lock on our `BackgroundScanner`'s state for the entire time. On my laptop, in the `linux` repo, this would often take around 13 seconds. When opening a file, Zed always refreshes the metadata for that file in its in-memory snapshot of worktree. This is normally very fast, but if another task is holding a lock on the `BackgroundScanner`, it blocks. ### Solution I've restructured how Zed handles Git statuses, so that when a git repository is updated, we recompute files' git statuses in fixed-sized batches. In between these batches, the `BackgroundScanner` is free to perform other work, so that file operations coming from the main thread will still be responsive. Release Notes: - Fixed a bug that caused long delays in opening files right after performing a commit in very large git repositories.
This commit is contained in:
parent
800c1ba916
commit
f7a86967fd
5 changed files with 384 additions and 450 deletions
|
@ -15,13 +15,7 @@ use pretty_assertions::assert_eq;
|
|||
use rand::prelude::*;
|
||||
use serde_json::json;
|
||||
use settings::{Settings, SettingsStore};
|
||||
use std::{
|
||||
env,
|
||||
fmt::Write,
|
||||
mem,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use std::{env, fmt::Write, mem, path::Path, sync::Arc};
|
||||
use util::{test::temp_tree, ResultExt};
|
||||
|
||||
#[gpui::test]
|
||||
|
@ -80,114 +74,6 @@ async fn test_traversal(cx: &mut TestAppContext) {
|
|||
})
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_descendent_entries(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.background_executor.clone());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
"a": "",
|
||||
"b": {
|
||||
"c": {
|
||||
"d": ""
|
||||
},
|
||||
"e": {}
|
||||
},
|
||||
"f": "",
|
||||
"g": {
|
||||
"h": {}
|
||||
},
|
||||
"i": {
|
||||
"j": {
|
||||
"k": ""
|
||||
},
|
||||
"l": {
|
||||
|
||||
}
|
||||
},
|
||||
".gitignore": "i/j\n",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let tree = Worktree::local(
|
||||
build_client(cx),
|
||||
Path::new("/root"),
|
||||
true,
|
||||
fs,
|
||||
Default::default(),
|
||||
&mut cx.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
|
||||
tree.read_with(cx, |tree, _| {
|
||||
assert_eq!(
|
||||
tree.descendent_entries(false, false, Path::new("b"))
|
||||
.map(|entry| entry.path.as_ref())
|
||||
.collect::<Vec<_>>(),
|
||||
vec![Path::new("b/c/d"),]
|
||||
);
|
||||
assert_eq!(
|
||||
tree.descendent_entries(true, false, Path::new("b"))
|
||||
.map(|entry| entry.path.as_ref())
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
Path::new("b"),
|
||||
Path::new("b/c"),
|
||||
Path::new("b/c/d"),
|
||||
Path::new("b/e"),
|
||||
]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
tree.descendent_entries(false, false, Path::new("g"))
|
||||
.map(|entry| entry.path.as_ref())
|
||||
.collect::<Vec<_>>(),
|
||||
Vec::<PathBuf>::new()
|
||||
);
|
||||
assert_eq!(
|
||||
tree.descendent_entries(true, false, Path::new("g"))
|
||||
.map(|entry| entry.path.as_ref())
|
||||
.collect::<Vec<_>>(),
|
||||
vec![Path::new("g"), Path::new("g/h"),]
|
||||
);
|
||||
});
|
||||
|
||||
// Expand gitignored directory.
|
||||
tree.read_with(cx, |tree, _| {
|
||||
tree.as_local()
|
||||
.unwrap()
|
||||
.refresh_entries_for_paths(vec![Path::new("i/j").into()])
|
||||
})
|
||||
.recv()
|
||||
.await;
|
||||
|
||||
tree.read_with(cx, |tree, _| {
|
||||
assert_eq!(
|
||||
tree.descendent_entries(false, false, Path::new("i"))
|
||||
.map(|entry| entry.path.as_ref())
|
||||
.collect::<Vec<_>>(),
|
||||
Vec::<PathBuf>::new()
|
||||
);
|
||||
assert_eq!(
|
||||
tree.descendent_entries(false, true, Path::new("i"))
|
||||
.map(|entry| entry.path.as_ref())
|
||||
.collect::<Vec<_>>(),
|
||||
vec![Path::new("i/j/k")]
|
||||
);
|
||||
assert_eq!(
|
||||
tree.descendent_entries(true, false, Path::new("i"))
|
||||
.map(|entry| entry.path.as_ref())
|
||||
.collect::<Vec<_>>(),
|
||||
vec![Path::new("i"), Path::new("i/l"),]
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_circular_symlinks(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
@ -2704,6 +2590,10 @@ fn check_worktree_entries(
|
|||
}
|
||||
|
||||
fn init_test(cx: &mut gpui::TestAppContext) {
|
||||
if std::env::var("RUST_LOG").is_ok() {
|
||||
env_logger::try_init().ok();
|
||||
}
|
||||
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue