git: Improve traversal perf with multiple repositories in a single worktree

This commit is contained in:
Piotr Osiewicz 2025-07-15 15:50:00 +02:00
parent 31a4f6d411
commit 0ae31d0dc7
4 changed files with 49 additions and 44 deletions

View file

@ -2570,8 +2570,8 @@ impl OutlinePanel {
let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs; let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs;
let active_multi_buffer = active_editor.read(cx).buffer().clone(); let active_multi_buffer = active_editor.read(cx).buffer().clone();
let new_entries = self.new_entries_for_fs_update.clone(); let new_entries = self.new_entries_for_fs_update.clone();
let repo_snapshots = self.project.update(cx, |project, cx| { let snapshots_by_abs_path = self.project.update(cx, |project, cx| {
project.git_store().read(cx).repo_snapshots(cx) project.git_store().read(cx).repo_snapshots_by_path(cx)
}); });
self.updating_fs_entries = true; self.updating_fs_entries = true;
self.fs_entries_update_task = cx.spawn_in(window, async move |outline_panel, cx| { self.fs_entries_update_task = cx.spawn_in(window, async move |outline_panel, cx| {
@ -2698,7 +2698,7 @@ impl OutlinePanel {
entry, entry,
}; };
let mut traversal = GitTraversal::new( let mut traversal = GitTraversal::new(
&repo_snapshots, &snapshots_by_abs_path,
worktree.traverse_from_path( worktree.traverse_from_path(
true, true,
true, true,

View file

@ -54,7 +54,7 @@ use std::{
ops::Range, ops::Range,
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::{ sync::{
Arc, OnceLock, Arc,
atomic::{self, AtomicU64}, atomic::{self, AtomicU64},
}, },
time::Instant, time::Instant,
@ -2211,6 +2211,13 @@ impl GitStore {
.map(|(id, repo)| (*id, repo.read(cx).snapshot.clone())) .map(|(id, repo)| (*id, repo.read(cx).snapshot.clone()))
.collect() .collect()
} }
pub fn repo_snapshots_by_path(&self, cx: &App) -> BTreeMap<Arc<Path>, RepositorySnapshot> {
self.repositories_by_abs_root_path
.iter()
.map(|(path, repo)| (path.clone(), repo.read(cx).snapshot.clone()))
.collect()
}
} }
impl BufferGitState { impl BufferGitState {

View file

@ -1,6 +1,10 @@
use collections::HashMap;
use git::status::GitSummary; use git::status::GitSummary;
use std::{ops::Deref, path::Path}; use std::{
collections::BTreeMap,
ops::{Deref, Range},
path::Path,
sync::Arc,
};
use sum_tree::Cursor; use sum_tree::Cursor;
use text::Bias; use text::Bias;
use worktree::{Entry, PathProgress, PathTarget, Traversal}; use worktree::{Entry, PathProgress, PathTarget, Traversal};
@ -11,18 +15,18 @@ use super::{RepositoryId, RepositorySnapshot, StatusEntry};
pub struct GitTraversal<'a> { pub struct GitTraversal<'a> {
traversal: Traversal<'a>, traversal: Traversal<'a>,
current_entry_summary: Option<GitSummary>, current_entry_summary: Option<GitSummary>,
repo_snapshots: &'a HashMap<RepositoryId, RepositorySnapshot>, snapshots_by_abs_path: &'a BTreeMap<Arc<Path>, RepositorySnapshot>,
repo_location: Option<(RepositoryId, Cursor<'a, StatusEntry, PathProgress<'a>>)>, repo_location: Option<(RepositoryId, Cursor<'a, StatusEntry, PathProgress<'a>>)>,
} }
impl<'a> GitTraversal<'a> { impl<'a> GitTraversal<'a> {
pub fn new( pub fn new(
repo_snapshots: &'a HashMap<RepositoryId, RepositorySnapshot>, snapshots_by_abs_path: &'a BTreeMap<Arc<Path>, RepositorySnapshot>,
traversal: Traversal<'a>, traversal: Traversal<'a>,
) -> GitTraversal<'a> { ) -> GitTraversal<'a> {
let mut this = GitTraversal { let mut this = GitTraversal {
traversal, traversal,
repo_snapshots, snapshots_by_abs_path,
current_entry_summary: None, current_entry_summary: None,
repo_location: None, repo_location: None,
}; };
@ -42,14 +46,15 @@ impl<'a> GitTraversal<'a> {
return; return;
}; };
let Some((repo, repo_path)) = self let range: Range<Arc<Path>> = Arc::from("".as_ref())..Arc::from(abs_path.as_ref());
.repo_snapshots let Some((repo, repo_path)) =
.values() self.snapshots_by_abs_path
.filter_map(|repo_snapshot| { .range(range)
let repo_path = repo_snapshot.abs_path_to_repo_path(&abs_path)?; .last()
Some((repo_snapshot, repo_path)) .and_then(|(_, repo)| {
}) let repo_path = repo.abs_path_to_repo_path(&abs_path)?;
.max_by_key(|(repo, _)| repo.work_directory_abs_path.clone()) Some((repo, repo_path))
})
else { else {
self.repo_location = None; self.repo_location = None;
return; return;
@ -145,12 +150,12 @@ pub struct ChildEntriesGitIter<'a> {
impl<'a> ChildEntriesGitIter<'a> { impl<'a> ChildEntriesGitIter<'a> {
pub fn new( pub fn new(
repo_snapshots: &'a HashMap<RepositoryId, RepositorySnapshot>, snapshots_by_abs_path: &'a BTreeMap<Arc<Path>, RepositorySnapshot>,
worktree_snapshot: &'a worktree::Snapshot, worktree_snapshot: &'a worktree::Snapshot,
parent_path: &'a Path, parent_path: &'a Path,
) -> Self { ) -> Self {
let mut traversal = GitTraversal::new( let mut traversal = GitTraversal::new(
repo_snapshots, snapshots_by_abs_path,
worktree_snapshot.traverse_from_path(true, true, true, parent_path), worktree_snapshot.traverse_from_path(true, true, true, parent_path),
); );
traversal.advance(); traversal.advance();
@ -310,7 +315,7 @@ mod tests {
let (repo_snapshots, worktree_snapshot) = project.read_with(cx, |project, cx| { let (repo_snapshots, worktree_snapshot) = project.read_with(cx, |project, cx| {
( (
project.git_store().read(cx).repo_snapshots(cx), project.git_store().read(cx).repo_snapshots_by_path(cx),
project.worktrees(cx).next().unwrap().read(cx).snapshot(), project.worktrees(cx).next().unwrap().read(cx).snapshot(),
) )
}); });
@ -385,7 +390,7 @@ mod tests {
let (repo_snapshots, worktree_snapshot) = project.read_with(cx, |project, cx| { let (repo_snapshots, worktree_snapshot) = project.read_with(cx, |project, cx| {
( (
project.git_store().read(cx).repo_snapshots(cx), project.git_store().read(cx).repo_snapshots_by_path(cx),
project.worktrees(cx).next().unwrap().read(cx).snapshot(), project.worktrees(cx).next().unwrap().read(cx).snapshot(),
) )
}); });
@ -511,7 +516,7 @@ mod tests {
let (repo_snapshots, worktree_snapshot) = project.read_with(cx, |project, cx| { let (repo_snapshots, worktree_snapshot) = project.read_with(cx, |project, cx| {
( (
project.git_store().read(cx).repo_snapshots(cx), project.git_store().read(cx).repo_snapshots_by_path(cx),
project.worktrees(cx).next().unwrap().read(cx).snapshot(), project.worktrees(cx).next().unwrap().read(cx).snapshot(),
) )
}); });
@ -620,7 +625,7 @@ mod tests {
let (repo_snapshots, worktree_snapshot) = project.read_with(cx, |project, cx| { let (repo_snapshots, worktree_snapshot) = project.read_with(cx, |project, cx| {
( (
project.git_store().read(cx).repo_snapshots(cx), project.git_store().read(cx).repo_snapshots_by_path(cx),
project.worktrees(cx).next().unwrap().read(cx).snapshot(), project.worktrees(cx).next().unwrap().read(cx).snapshot(),
) )
}); });
@ -748,7 +753,7 @@ mod tests {
let (repo_snapshots, worktree_snapshot) = project.read_with(cx, |project, cx| { let (repo_snapshots, worktree_snapshot) = project.read_with(cx, |project, cx| {
( (
project.git_store().read(cx).repo_snapshots(cx), project.git_store().read(cx).repo_snapshots_by_path(cx),
project.worktrees(cx).next().unwrap().read(cx).snapshot(), project.worktrees(cx).next().unwrap().read(cx).snapshot(),
) )
}); });
@ -766,7 +771,7 @@ mod tests {
#[track_caller] #[track_caller]
fn check_git_statuses( fn check_git_statuses(
repo_snapshots: &HashMap<RepositoryId, RepositorySnapshot>, repo_snapshots: &std::collections::BTreeMap<Arc<Path>, RepositorySnapshot>,
worktree_snapshot: &worktree::Snapshot, worktree_snapshot: &worktree::Snapshot,
expected_statuses: &[(&Path, GitSummary)], expected_statuses: &[(&Path, GitSummary)],
) { ) {

View file

@ -1806,11 +1806,11 @@ impl ProjectPanel {
let parent_entry = worktree.entry_for_path(parent_path)?; let parent_entry = worktree.entry_for_path(parent_path)?;
// Remove all siblings that are being deleted except the last marked entry // Remove all siblings that are being deleted except the last marked entry
let repo_snapshots = git_store.repo_snapshots(cx); let snapshots_by_root_path = git_store.repo_snapshots_by_path(cx);
let worktree_snapshot = worktree.snapshot(); let worktree_snapshot = worktree.snapshot();
let hide_gitignore = ProjectPanelSettings::get_global(cx).hide_gitignore; let hide_gitignore = ProjectPanelSettings::get_global(cx).hide_gitignore;
let mut siblings: Vec<_> = let mut siblings: Vec<_> =
ChildEntriesGitIter::new(&repo_snapshots, &worktree_snapshot, parent_path) ChildEntriesGitIter::new(&snapshots_by_root_path, &worktree_snapshot, parent_path)
.filter(|sibling| { .filter(|sibling| {
(sibling.id == latest_entry.id) (sibling.id == latest_entry.id)
|| (!marked_entries_in_worktree.contains(&&SelectedEntry { || (!marked_entries_in_worktree.contains(&&SelectedEntry {
@ -2858,7 +2858,8 @@ impl ProjectPanel {
let auto_collapse_dirs = settings.auto_fold_dirs; let auto_collapse_dirs = settings.auto_fold_dirs;
let hide_gitignore = settings.hide_gitignore; let hide_gitignore = settings.hide_gitignore;
let project = self.project.read(cx); let project = self.project.read(cx);
let repo_snapshots = project.git_store().read(cx).repo_snapshots(cx); let git_store = project.git_store().read(cx);
let snapshots_by_root_path = git_store.repo_snapshots_by_path(cx);
self.last_worktree_root_id = project self.last_worktree_root_id = project
.visible_worktrees(cx) .visible_worktrees(cx)
.next_back() .next_back()
@ -2903,7 +2904,7 @@ impl ProjectPanel {
let mut visible_worktree_entries = Vec::new(); let mut visible_worktree_entries = Vec::new();
let mut entry_iter = let mut entry_iter =
GitTraversal::new(&repo_snapshots, worktree_snapshot.entries(true, 0)); GitTraversal::new(&snapshots_by_root_path, worktree_snapshot.entries(true, 0));
let mut auto_folded_ancestors = vec![]; let mut auto_folded_ancestors = vec![];
while let Some(entry) = entry_iter.entry() { while let Some(entry) = entry_iter.entry() {
if hide_root && Some(entry.entry) == worktree.read(cx).root_entry() { if hide_root && Some(entry.entry) == worktree.read(cx).root_entry() {
@ -3503,16 +3504,12 @@ impl ProjectPanel {
.cloned(); .cloned();
} }
let repo_snapshots = self let git_store = self.project.read(cx).git_store().read(cx);
.project let snapshots_by_root_path = git_store.repo_snapshots_by_path(cx);
.read(cx)
.git_store()
.read(cx)
.repo_snapshots(cx);
let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?; let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?;
worktree.read_with(cx, |tree, _| { worktree.read_with(cx, |tree, _| {
utils::ReversibleIterable::new( utils::ReversibleIterable::new(
GitTraversal::new(&repo_snapshots, tree.entries(true, 0usize)), GitTraversal::new(&snapshots_by_root_path, tree.entries(true, 0usize)),
reverse_search, reverse_search,
) )
.find_single_ended(|ele| predicate(*ele, worktree_id)) .find_single_ended(|ele| predicate(*ele, worktree_id))
@ -3532,12 +3529,8 @@ impl ProjectPanel {
.iter() .iter()
.map(|(worktree_id, _, _)| *worktree_id) .map(|(worktree_id, _, _)| *worktree_id)
.collect(); .collect();
let repo_snapshots = self let git_store = self.project.read(cx).git_store().read(cx);
.project let snapshots_by_root_path = git_store.repo_snapshots_by_path(cx);
.read(cx)
.git_store()
.read(cx)
.repo_snapshots(cx);
let mut last_found: Option<SelectedEntry> = None; let mut last_found: Option<SelectedEntry> = None;
@ -3554,7 +3547,7 @@ impl ProjectPanel {
let tree_id = worktree.id(); let tree_id = worktree.id();
let mut first_iter = GitTraversal::new( let mut first_iter = GitTraversal::new(
&repo_snapshots, &snapshots_by_root_path,
worktree.traverse_from_path(true, true, true, entry.path.as_ref()), worktree.traverse_from_path(true, true, true, entry.path.as_ref()),
); );
@ -3570,7 +3563,7 @@ impl ProjectPanel {
.map(|ele| ele.to_owned()); .map(|ele| ele.to_owned());
let second_iter = let second_iter =
GitTraversal::new(&repo_snapshots, worktree.entries(true, 0usize)); GitTraversal::new(&snapshots_by_root_path, worktree.entries(true, 0usize));
let second = if reverse_search { let second = if reverse_search {
second_iter second_iter