Project panel faster (#35634)

- **Use a struct instead of a thruple for visible worktree entries**
- **Try some telemetry**

Closes #ISSUE

Release Notes:

- N/A

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
This commit is contained in:
Conrad Irwin 2025-08-08 13:32:58 +01:00 committed by GitHub
parent 0097d89672
commit bc32b5a976
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 122 additions and 78 deletions

1
Cargo.lock generated
View file

@ -12653,6 +12653,7 @@ dependencies = [
"serde_json", "serde_json",
"settings", "settings",
"smallvec", "smallvec",
"telemetry",
"theme", "theme",
"ui", "ui",
"util", "util",

View file

@ -110,11 +110,7 @@ impl<'a> GitTraversal<'a> {
} }
pub fn advance(&mut self) -> bool { pub fn advance(&mut self) -> bool {
self.advance_by(1) let found = self.traversal.advance_by(1);
}
pub fn advance_by(&mut self, count: usize) -> bool {
let found = self.traversal.advance_by(count);
self.synchronize_statuses(false); self.synchronize_statuses(false);
found found
} }

View file

@ -41,6 +41,7 @@ worktree.workspace = true
workspace.workspace = true workspace.workspace = true
language.workspace = true language.workspace = true
zed_actions.workspace = true zed_actions.workspace = true
telemetry.workspace = true
workspace-hack.workspace = true workspace-hack.workspace = true
[dev-dependencies] [dev-dependencies]

View file

@ -44,7 +44,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore, update_settings_file}; use settings::{Settings, SettingsStore, update_settings_file};
use smallvec::SmallVec; use smallvec::SmallVec;
use std::any::TypeId; use std::{any::TypeId, time::Instant};
use std::{ use std::{
cell::OnceCell, cell::OnceCell,
cmp, cmp,
@ -74,6 +74,12 @@ use zed_actions::OpenRecent;
const PROJECT_PANEL_KEY: &str = "ProjectPanel"; const PROJECT_PANEL_KEY: &str = "ProjectPanel";
const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX; const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX;
struct VisibleEntriesForWorktree {
worktree_id: WorktreeId,
entries: Vec<GitEntry>,
index: OnceCell<HashSet<Arc<Path>>>,
}
pub struct ProjectPanel { pub struct ProjectPanel {
project: Entity<Project>, project: Entity<Project>,
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
@ -82,7 +88,7 @@ pub struct ProjectPanel {
// An update loop that keeps incrementing/decrementing scroll offset while there is a dragged entry that's // An update loop that keeps incrementing/decrementing scroll offset while there is a dragged entry that's
// hovered over the start/end of a list. // hovered over the start/end of a list.
hover_scroll_task: Option<Task<()>>, hover_scroll_task: Option<Task<()>>,
visible_entries: Vec<(WorktreeId, Vec<GitEntry>, OnceCell<HashSet<Arc<Path>>>)>, visible_entries: Vec<VisibleEntriesForWorktree>,
/// Maps from leaf project entry ID to the currently selected ancestor. /// Maps from leaf project entry ID to the currently selected ancestor.
/// Relevant only for auto-fold dirs, where a single project panel entry may actually consist of several /// Relevant only for auto-fold dirs, where a single project panel entry may actually consist of several
/// project entries (and all non-leaf nodes are guaranteed to be directories). /// project entries (and all non-leaf nodes are guaranteed to be directories).
@ -116,6 +122,7 @@ pub struct ProjectPanel {
hover_expand_task: Option<Task<()>>, hover_expand_task: Option<Task<()>>,
previous_drag_position: Option<Point<Pixels>>, previous_drag_position: Option<Point<Pixels>>,
sticky_items_count: usize, sticky_items_count: usize,
last_reported_update: Instant,
} }
struct DragTargetEntry { struct DragTargetEntry {
@ -631,6 +638,7 @@ impl ProjectPanel {
hover_expand_task: None, hover_expand_task: None,
previous_drag_position: None, previous_drag_position: None,
sticky_items_count: 0, sticky_items_count: 0,
last_reported_update: Instant::now(),
}; };
this.update_visible_entries(None, cx); this.update_visible_entries(None, cx);
@ -1266,15 +1274,19 @@ impl ProjectPanel {
entry_ix -= 1; entry_ix -= 1;
} else if worktree_ix > 0 { } else if worktree_ix > 0 {
worktree_ix -= 1; worktree_ix -= 1;
entry_ix = self.visible_entries[worktree_ix].1.len() - 1; entry_ix = self.visible_entries[worktree_ix].entries.len() - 1;
} else { } else {
return; return;
} }
let (worktree_id, worktree_entries, _) = &self.visible_entries[worktree_ix]; let VisibleEntriesForWorktree {
worktree_id,
entries,
..
} = &self.visible_entries[worktree_ix];
let selection = SelectedEntry { let selection = SelectedEntry {
worktree_id: *worktree_id, worktree_id: *worktree_id,
entry_id: worktree_entries[entry_ix].id, entry_id: entries[entry_ix].id,
}; };
self.selection = Some(selection); self.selection = Some(selection);
if window.modifiers().shift { if window.modifiers().shift {
@ -2005,7 +2017,9 @@ impl ProjectPanel {
if let Some(selection) = self.selection { if let Some(selection) = self.selection {
let (mut worktree_ix, mut entry_ix, _) = let (mut worktree_ix, mut entry_ix, _) =
self.index_for_selection(selection).unwrap_or_default(); self.index_for_selection(selection).unwrap_or_default();
if let Some((_, worktree_entries, _)) = self.visible_entries.get(worktree_ix) { if let Some(worktree_entries) =
self.visible_entries.get(worktree_ix).map(|v| &v.entries)
{
if entry_ix + 1 < worktree_entries.len() { if entry_ix + 1 < worktree_entries.len() {
entry_ix += 1; entry_ix += 1;
} else { } else {
@ -2014,9 +2028,13 @@ impl ProjectPanel {
} }
} }
if let Some((worktree_id, worktree_entries, _)) = self.visible_entries.get(worktree_ix) if let Some(VisibleEntriesForWorktree {
worktree_id,
entries,
..
}) = self.visible_entries.get(worktree_ix)
{ {
if let Some(entry) = worktree_entries.get(entry_ix) { if let Some(entry) = entries.get(entry_ix) {
let selection = SelectedEntry { let selection = SelectedEntry {
worktree_id: *worktree_id, worktree_id: *worktree_id,
entry_id: entry.id, entry_id: entry.id,
@ -2252,8 +2270,13 @@ impl ProjectPanel {
} }
fn select_first(&mut self, _: &SelectFirst, window: &mut Window, cx: &mut Context<Self>) { fn select_first(&mut self, _: &SelectFirst, window: &mut Window, cx: &mut Context<Self>) {
if let Some((worktree_id, visible_worktree_entries, _)) = self.visible_entries.first() { if let Some(VisibleEntriesForWorktree {
if let Some(entry) = visible_worktree_entries.first() { worktree_id,
entries,
..
}) = self.visible_entries.first()
{
if let Some(entry) = entries.first() {
let selection = SelectedEntry { let selection = SelectedEntry {
worktree_id: *worktree_id, worktree_id: *worktree_id,
entry_id: entry.id, entry_id: entry.id,
@ -2269,9 +2292,14 @@ impl ProjectPanel {
} }
fn select_last(&mut self, _: &SelectLast, _: &mut Window, cx: &mut Context<Self>) { fn select_last(&mut self, _: &SelectLast, _: &mut Window, cx: &mut Context<Self>) {
if let Some((worktree_id, visible_worktree_entries, _)) = self.visible_entries.last() { if let Some(VisibleEntriesForWorktree {
worktree_id,
entries,
..
}) = self.visible_entries.last()
{
let worktree = self.project.read(cx).worktree_for_id(*worktree_id, cx); let worktree = self.project.read(cx).worktree_for_id(*worktree_id, cx);
if let (Some(worktree), Some(entry)) = (worktree, visible_worktree_entries.last()) { if let (Some(worktree), Some(entry)) = (worktree, entries.last()) {
let worktree = worktree.read(cx); let worktree = worktree.read(cx);
if let Some(entry) = worktree.entry_for_id(entry.id) { if let Some(entry) = worktree.entry_for_id(entry.id) {
let selection = SelectedEntry { let selection = SelectedEntry {
@ -2960,6 +2988,7 @@ impl ProjectPanel {
new_selected_entry: Option<(WorktreeId, ProjectEntryId)>, new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
let now = Instant::now();
let settings = ProjectPanelSettings::get_global(cx); let settings = ProjectPanelSettings::get_global(cx);
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;
@ -3157,19 +3186,23 @@ impl ProjectPanel {
project::sort_worktree_entries(&mut visible_worktree_entries); project::sort_worktree_entries(&mut visible_worktree_entries);
self.visible_entries self.visible_entries.push(VisibleEntriesForWorktree {
.push((worktree_id, visible_worktree_entries, OnceCell::new())); worktree_id,
entries: visible_worktree_entries,
index: OnceCell::new(),
})
} }
if let Some((project_entry_id, worktree_id, _)) = max_width_item { if let Some((project_entry_id, worktree_id, _)) = max_width_item {
let mut visited_worktrees_length = 0; let mut visited_worktrees_length = 0;
let index = self.visible_entries.iter().find_map(|(id, entries, _)| { let index = self.visible_entries.iter().find_map(|visible_entries| {
if worktree_id == *id { if worktree_id == visible_entries.worktree_id {
entries visible_entries
.entries
.iter() .iter()
.position(|entry| entry.id == project_entry_id) .position(|entry| entry.id == project_entry_id)
} else { } else {
visited_worktrees_length += entries.len(); visited_worktrees_length += visible_entries.entries.len();
None None
} }
}); });
@ -3183,6 +3216,18 @@ impl ProjectPanel {
entry_id, entry_id,
}); });
} }
let elapsed = now.elapsed();
if self.last_reported_update.elapsed() > Duration::from_secs(3600) {
telemetry::event!(
"Project Panel Updated",
elapsed_ms = elapsed.as_millis() as u64,
worktree_entries = self
.visible_entries
.iter()
.map(|worktree| worktree.entries.len())
.sum::<usize>(),
)
}
} }
fn expand_entry( fn expand_entry(
@ -3396,15 +3441,14 @@ impl ProjectPanel {
worktree_id: WorktreeId, worktree_id: WorktreeId,
) -> Option<(usize, usize, usize)> { ) -> Option<(usize, usize, usize)> {
let mut total_ix = 0; let mut total_ix = 0;
for (worktree_ix, (current_worktree_id, visible_worktree_entries, _)) in for (worktree_ix, visible) in self.visible_entries.iter().enumerate() {
self.visible_entries.iter().enumerate() if worktree_id != visible.worktree_id {
{ total_ix += visible.entries.len();
if worktree_id != *current_worktree_id {
total_ix += visible_worktree_entries.len();
continue; continue;
} }
return visible_worktree_entries return visible
.entries
.iter() .iter()
.enumerate() .enumerate()
.find(|(_, entry)| entry.id == entry_id) .find(|(_, entry)| entry.id == entry_id)
@ -3415,12 +3459,13 @@ impl ProjectPanel {
fn entry_at_index(&self, index: usize) -> Option<(WorktreeId, GitEntryRef<'_>)> { fn entry_at_index(&self, index: usize) -> Option<(WorktreeId, GitEntryRef<'_>)> {
let mut offset = 0; let mut offset = 0;
for (worktree_id, visible_worktree_entries, _) in &self.visible_entries { for worktree in &self.visible_entries {
let current_len = visible_worktree_entries.len(); let current_len = worktree.entries.len();
if index < offset + current_len { if index < offset + current_len {
return visible_worktree_entries return worktree
.entries
.get(index - offset) .get(index - offset)
.map(|entry| (*worktree_id, entry.to_ref())); .map(|entry| (worktree.worktree_id, entry.to_ref()));
} }
offset += current_len; offset += current_len;
} }
@ -3441,26 +3486,23 @@ impl ProjectPanel {
), ),
) { ) {
let mut ix = 0; let mut ix = 0;
for (_, visible_worktree_entries, entries_paths) in &self.visible_entries { for visible in &self.visible_entries {
if ix >= range.end { if ix >= range.end {
return; return;
} }
if ix + visible_worktree_entries.len() <= range.start { if ix + visible.entries.len() <= range.start {
ix += visible_worktree_entries.len(); ix += visible.entries.len();
continue; continue;
} }
let end_ix = range.end.min(ix + visible_worktree_entries.len()); let end_ix = range.end.min(ix + visible.entries.len());
let entry_range = range.start.saturating_sub(ix)..end_ix - ix; let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
let entries = entries_paths.get_or_init(|| { let entries = visible
visible_worktree_entries .index
.iter() .get_or_init(|| visible.entries.iter().map(|e| (e.path.clone())).collect());
.map(|e| (e.path.clone()))
.collect()
});
let base_index = ix + entry_range.start; let base_index = ix + entry_range.start;
for (i, entry) in visible_worktree_entries[entry_range].iter().enumerate() { for (i, entry) in visible.entries[entry_range].iter().enumerate() {
let global_index = base_index + i; let global_index = base_index + i;
callback(&entry, global_index, entries, window, cx); callback(&entry, global_index, entries, window, cx);
} }
@ -3476,40 +3518,41 @@ impl ProjectPanel {
mut callback: impl FnMut(ProjectEntryId, EntryDetails, &mut Window, &mut Context<ProjectPanel>), mut callback: impl FnMut(ProjectEntryId, EntryDetails, &mut Window, &mut Context<ProjectPanel>),
) { ) {
let mut ix = 0; let mut ix = 0;
for (worktree_id, visible_worktree_entries, entries_paths) in &self.visible_entries { for visible in &self.visible_entries {
if ix >= range.end { if ix >= range.end {
return; return;
} }
if ix + visible_worktree_entries.len() <= range.start { if ix + visible.entries.len() <= range.start {
ix += visible_worktree_entries.len(); ix += visible.entries.len();
continue; continue;
} }
let end_ix = range.end.min(ix + visible_worktree_entries.len()); let end_ix = range.end.min(ix + visible.entries.len());
let git_status_setting = { let git_status_setting = {
let settings = ProjectPanelSettings::get_global(cx); let settings = ProjectPanelSettings::get_global(cx);
settings.git_status settings.git_status
}; };
if let Some(worktree) = self.project.read(cx).worktree_for_id(*worktree_id, cx) { if let Some(worktree) = self
.project
.read(cx)
.worktree_for_id(visible.worktree_id, cx)
{
let snapshot = worktree.read(cx).snapshot(); let snapshot = worktree.read(cx).snapshot();
let root_name = OsStr::new(snapshot.root_name()); let root_name = OsStr::new(snapshot.root_name());
let entry_range = range.start.saturating_sub(ix)..end_ix - ix; let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
let entries = entries_paths.get_or_init(|| { let entries = visible
visible_worktree_entries .index
.iter() .get_or_init(|| visible.entries.iter().map(|e| (e.path.clone())).collect());
.map(|e| (e.path.clone())) for entry in visible.entries[entry_range].iter() {
.collect()
});
for entry in visible_worktree_entries[entry_range].iter() {
let status = git_status_setting let status = git_status_setting
.then_some(entry.git_summary) .then_some(entry.git_summary)
.unwrap_or_default(); .unwrap_or_default();
let mut details = self.details_for_entry( let mut details = self.details_for_entry(
entry, entry,
*worktree_id, visible.worktree_id,
root_name, root_name,
entries, entries,
status, status,
@ -3595,9 +3638,9 @@ impl ProjectPanel {
let entries = self let entries = self
.visible_entries .visible_entries
.iter() .iter()
.find_map(|(tree_id, entries, _)| { .find_map(|visible| {
if worktree_id == *tree_id { if worktree_id == visible.worktree_id {
Some(entries) Some(&visible.entries)
} else { } else {
None None
} }
@ -3636,7 +3679,7 @@ impl ProjectPanel {
let mut worktree_ids: Vec<_> = self let mut worktree_ids: Vec<_> = self
.visible_entries .visible_entries
.iter() .iter()
.map(|(worktree_id, _, _)| *worktree_id) .map(|worktree| worktree.worktree_id)
.collect(); .collect();
let repo_snapshots = self let repo_snapshots = self
.project .project
@ -3752,7 +3795,7 @@ impl ProjectPanel {
let mut worktree_ids: Vec<_> = self let mut worktree_ids: Vec<_> = self
.visible_entries .visible_entries
.iter() .iter()
.map(|(worktree_id, _, _)| *worktree_id) .map(|worktree| worktree.worktree_id)
.collect(); .collect();
let mut last_found: Option<SelectedEntry> = None; let mut last_found: Option<SelectedEntry> = None;
@ -3761,8 +3804,8 @@ impl ProjectPanel {
let entries = self let entries = self
.visible_entries .visible_entries
.iter() .iter()
.find(|(worktree_id, _, _)| *worktree_id == start.worktree_id) .find(|worktree| worktree.worktree_id == start.worktree_id)
.map(|(_, entries, _)| entries)?; .map(|worktree| &worktree.entries)?;
let mut start_idx = entries let mut start_idx = entries
.iter() .iter()
@ -4914,7 +4957,7 @@ impl ProjectPanel {
let (active_indent_range, depth) = { let (active_indent_range, depth) = {
let (worktree_ix, child_offset, ix) = self.index_for_entry(entry.id, worktree.id())?; let (worktree_ix, child_offset, ix) = self.index_for_entry(entry.id, worktree.id())?;
let child_paths = &self.visible_entries[worktree_ix].1; let child_paths = &self.visible_entries[worktree_ix].entries;
let mut child_count = 0; let mut child_count = 0;
let depth = entry.path.ancestors().count(); let depth = entry.path.ancestors().count();
while let Some(entry) = child_paths.get(child_offset + child_count + 1) { while let Some(entry) = child_paths.get(child_offset + child_count + 1) {
@ -4927,9 +4970,14 @@ impl ProjectPanel {
let start = ix + 1; let start = ix + 1;
let end = start + child_count; let end = start + child_count;
let (_, entries, paths) = &self.visible_entries[worktree_ix]; let visible_worktree = &self.visible_entries[worktree_ix];
let visible_worktree_entries = let visible_worktree_entries = visible_worktree.index.get_or_init(|| {
paths.get_or_init(|| entries.iter().map(|e| (e.path.clone())).collect()); visible_worktree
.entries
.iter()
.map(|e| (e.path.clone()))
.collect()
});
// Calculate the actual depth of the entry, taking into account that directories can be auto-folded. // Calculate the actual depth of the entry, taking into account that directories can be auto-folded.
let (depth, _) = Self::calculate_depth_and_difference(entry, visible_worktree_entries); let (depth, _) = Self::calculate_depth_and_difference(entry, visible_worktree_entries);
@ -4964,10 +5012,10 @@ impl ProjectPanel {
return SmallVec::new(); return SmallVec::new();
}; };
let Some((_, visible_worktree_entries, entries_paths)) = self let Some(visible) = self
.visible_entries .visible_entries
.iter() .iter()
.find(|(id, _, _)| *id == worktree_id) .find(|worktree| worktree.worktree_id == worktree_id)
else { else {
return SmallVec::new(); return SmallVec::new();
}; };
@ -4977,12 +5025,9 @@ impl ProjectPanel {
}; };
let worktree = worktree.read(cx).snapshot(); let worktree = worktree.read(cx).snapshot();
let paths = entries_paths.get_or_init(|| { let paths = visible
visible_worktree_entries .index
.iter() .get_or_init(|| visible.entries.iter().map(|e| e.path.clone()).collect());
.map(|e| e.path.clone())
.collect()
});
let mut sticky_parents = Vec::new(); let mut sticky_parents = Vec::new();
let mut current_path = entry_ref.path.clone(); let mut current_path = entry_ref.path.clone();
@ -5012,7 +5057,8 @@ impl ProjectPanel {
let root_name = OsStr::new(worktree.root_name()); let root_name = OsStr::new(worktree.root_name());
let git_summaries_by_id = if git_status_enabled { let git_summaries_by_id = if git_status_enabled {
visible_worktree_entries visible
.entries
.iter() .iter()
.map(|e| (e.id, e.git_summary)) .map(|e| (e.id, e.git_summary))
.collect::<HashMap<_, _>>() .collect::<HashMap<_, _>>()
@ -5110,7 +5156,7 @@ impl Render for ProjectPanel {
let item_count = self let item_count = self
.visible_entries .visible_entries
.iter() .iter()
.map(|(_, worktree_entries, _)| worktree_entries.len()) .map(|worktree| worktree.entries.len())
.sum(); .sum();
fn handle_drag_move<T: 'static>( fn handle_drag_move<T: 'static>(