From bc32b5a976436f941b7ba9a38f64e80dbcebb268 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 8 Aug 2025 13:32:58 +0100 Subject: [PATCH] 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> --- Cargo.lock | 1 + crates/project/src/git_store/git_traversal.rs | 6 +- crates/project_panel/Cargo.toml | 1 + crates/project_panel/src/project_panel.rs | 192 +++++++++++------- 4 files changed, 122 insertions(+), 78 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e63c5e2acf..0f0e78bb48 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12653,6 +12653,7 @@ dependencies = [ "serde_json", "settings", "smallvec", + "telemetry", "theme", "ui", "util", diff --git a/crates/project/src/git_store/git_traversal.rs b/crates/project/src/git_store/git_traversal.rs index 777042cb02..bbcffe046d 100644 --- a/crates/project/src/git_store/git_traversal.rs +++ b/crates/project/src/git_store/git_traversal.rs @@ -110,11 +110,7 @@ impl<'a> GitTraversal<'a> { } pub fn advance(&mut self) -> bool { - self.advance_by(1) - } - - pub fn advance_by(&mut self, count: usize) -> bool { - let found = self.traversal.advance_by(count); + let found = self.traversal.advance_by(1); self.synchronize_statuses(false); found } diff --git a/crates/project_panel/Cargo.toml b/crates/project_panel/Cargo.toml index b9d43d9873..6ad3c4c2cd 100644 --- a/crates/project_panel/Cargo.toml +++ b/crates/project_panel/Cargo.toml @@ -41,6 +41,7 @@ worktree.workspace = true workspace.workspace = true language.workspace = true zed_actions.workspace = true +telemetry.workspace = true workspace-hack.workspace = true [dev-dependencies] diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 94d543ed0c..967df41e23 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -44,7 +44,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsStore, update_settings_file}; use smallvec::SmallVec; -use std::any::TypeId; +use std::{any::TypeId, time::Instant}; use std::{ cell::OnceCell, cmp, @@ -74,6 +74,12 @@ use zed_actions::OpenRecent; const PROJECT_PANEL_KEY: &str = "ProjectPanel"; const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX; +struct VisibleEntriesForWorktree { + worktree_id: WorktreeId, + entries: Vec, + index: OnceCell>>, +} + pub struct ProjectPanel { project: Entity, fs: Arc, @@ -82,7 +88,7 @@ pub struct ProjectPanel { // 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. hover_scroll_task: Option>, - visible_entries: Vec<(WorktreeId, Vec, OnceCell>>)>, + visible_entries: Vec, /// 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 /// project entries (and all non-leaf nodes are guaranteed to be directories). @@ -116,6 +122,7 @@ pub struct ProjectPanel { hover_expand_task: Option>, previous_drag_position: Option>, sticky_items_count: usize, + last_reported_update: Instant, } struct DragTargetEntry { @@ -631,6 +638,7 @@ impl ProjectPanel { hover_expand_task: None, previous_drag_position: None, sticky_items_count: 0, + last_reported_update: Instant::now(), }; this.update_visible_entries(None, cx); @@ -1266,15 +1274,19 @@ impl ProjectPanel { entry_ix -= 1; } else if worktree_ix > 0 { worktree_ix -= 1; - entry_ix = self.visible_entries[worktree_ix].1.len() - 1; + entry_ix = self.visible_entries[worktree_ix].entries.len() - 1; } else { return; } - let (worktree_id, worktree_entries, _) = &self.visible_entries[worktree_ix]; + let VisibleEntriesForWorktree { + worktree_id, + entries, + .. + } = &self.visible_entries[worktree_ix]; let selection = SelectedEntry { worktree_id: *worktree_id, - entry_id: worktree_entries[entry_ix].id, + entry_id: entries[entry_ix].id, }; self.selection = Some(selection); if window.modifiers().shift { @@ -2005,7 +2017,9 @@ impl ProjectPanel { if let Some(selection) = self.selection { let (mut worktree_ix, mut entry_ix, _) = 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() { entry_ix += 1; } 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 { worktree_id: *worktree_id, entry_id: entry.id, @@ -2252,8 +2270,13 @@ impl ProjectPanel { } fn select_first(&mut self, _: &SelectFirst, window: &mut Window, cx: &mut Context) { - if let Some((worktree_id, visible_worktree_entries, _)) = self.visible_entries.first() { - if let Some(entry) = visible_worktree_entries.first() { + if let Some(VisibleEntriesForWorktree { + worktree_id, + entries, + .. + }) = self.visible_entries.first() + { + if let Some(entry) = entries.first() { let selection = SelectedEntry { worktree_id: *worktree_id, entry_id: entry.id, @@ -2269,9 +2292,14 @@ impl ProjectPanel { } fn select_last(&mut self, _: &SelectLast, _: &mut Window, cx: &mut Context) { - 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); - 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); if let Some(entry) = worktree.entry_for_id(entry.id) { let selection = SelectedEntry { @@ -2960,6 +2988,7 @@ impl ProjectPanel { new_selected_entry: Option<(WorktreeId, ProjectEntryId)>, cx: &mut Context, ) { + let now = Instant::now(); let settings = ProjectPanelSettings::get_global(cx); let auto_collapse_dirs = settings.auto_fold_dirs; let hide_gitignore = settings.hide_gitignore; @@ -3157,19 +3186,23 @@ impl ProjectPanel { project::sort_worktree_entries(&mut visible_worktree_entries); - self.visible_entries - .push((worktree_id, visible_worktree_entries, OnceCell::new())); + self.visible_entries.push(VisibleEntriesForWorktree { + worktree_id, + entries: visible_worktree_entries, + index: OnceCell::new(), + }) } if let Some((project_entry_id, worktree_id, _)) = max_width_item { let mut visited_worktrees_length = 0; - let index = self.visible_entries.iter().find_map(|(id, entries, _)| { - if worktree_id == *id { - entries + let index = self.visible_entries.iter().find_map(|visible_entries| { + if worktree_id == visible_entries.worktree_id { + visible_entries + .entries .iter() .position(|entry| entry.id == project_entry_id) } else { - visited_worktrees_length += entries.len(); + visited_worktrees_length += visible_entries.entries.len(); None } }); @@ -3183,6 +3216,18 @@ impl ProjectPanel { 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::(), + ) + } } fn expand_entry( @@ -3396,15 +3441,14 @@ impl ProjectPanel { worktree_id: WorktreeId, ) -> Option<(usize, usize, usize)> { let mut total_ix = 0; - for (worktree_ix, (current_worktree_id, visible_worktree_entries, _)) in - self.visible_entries.iter().enumerate() - { - if worktree_id != *current_worktree_id { - total_ix += visible_worktree_entries.len(); + for (worktree_ix, visible) in self.visible_entries.iter().enumerate() { + if worktree_id != visible.worktree_id { + total_ix += visible.entries.len(); continue; } - return visible_worktree_entries + return visible + .entries .iter() .enumerate() .find(|(_, entry)| entry.id == entry_id) @@ -3415,12 +3459,13 @@ impl ProjectPanel { fn entry_at_index(&self, index: usize) -> Option<(WorktreeId, GitEntryRef<'_>)> { let mut offset = 0; - for (worktree_id, visible_worktree_entries, _) in &self.visible_entries { - let current_len = visible_worktree_entries.len(); + for worktree in &self.visible_entries { + let current_len = worktree.entries.len(); if index < offset + current_len { - return visible_worktree_entries + return worktree + .entries .get(index - offset) - .map(|entry| (*worktree_id, entry.to_ref())); + .map(|entry| (worktree.worktree_id, entry.to_ref())); } offset += current_len; } @@ -3441,26 +3486,23 @@ impl ProjectPanel { ), ) { let mut ix = 0; - for (_, visible_worktree_entries, entries_paths) in &self.visible_entries { + for visible in &self.visible_entries { if ix >= range.end { return; } - if ix + visible_worktree_entries.len() <= range.start { - ix += visible_worktree_entries.len(); + if ix + visible.entries.len() <= range.start { + ix += visible.entries.len(); 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 entries = entries_paths.get_or_init(|| { - visible_worktree_entries - .iter() - .map(|e| (e.path.clone())) - .collect() - }); + let entries = visible + .index + .get_or_init(|| visible.entries.iter().map(|e| (e.path.clone())).collect()); 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; callback(&entry, global_index, entries, window, cx); } @@ -3476,40 +3518,41 @@ impl ProjectPanel { mut callback: impl FnMut(ProjectEntryId, EntryDetails, &mut Window, &mut Context), ) { 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 { return; } - if ix + visible_worktree_entries.len() <= range.start { - ix += visible_worktree_entries.len(); + if ix + visible.entries.len() <= range.start { + ix += visible.entries.len(); 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 settings = ProjectPanelSettings::get_global(cx); 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 root_name = OsStr::new(snapshot.root_name()); let entry_range = range.start.saturating_sub(ix)..end_ix - ix; - let entries = entries_paths.get_or_init(|| { - visible_worktree_entries - .iter() - .map(|e| (e.path.clone())) - .collect() - }); - for entry in visible_worktree_entries[entry_range].iter() { + let entries = visible + .index + .get_or_init(|| visible.entries.iter().map(|e| (e.path.clone())).collect()); + for entry in visible.entries[entry_range].iter() { let status = git_status_setting .then_some(entry.git_summary) .unwrap_or_default(); let mut details = self.details_for_entry( entry, - *worktree_id, + visible.worktree_id, root_name, entries, status, @@ -3595,9 +3638,9 @@ impl ProjectPanel { let entries = self .visible_entries .iter() - .find_map(|(tree_id, entries, _)| { - if worktree_id == *tree_id { - Some(entries) + .find_map(|visible| { + if worktree_id == visible.worktree_id { + Some(&visible.entries) } else { None } @@ -3636,7 +3679,7 @@ impl ProjectPanel { let mut worktree_ids: Vec<_> = self .visible_entries .iter() - .map(|(worktree_id, _, _)| *worktree_id) + .map(|worktree| worktree.worktree_id) .collect(); let repo_snapshots = self .project @@ -3752,7 +3795,7 @@ impl ProjectPanel { let mut worktree_ids: Vec<_> = self .visible_entries .iter() - .map(|(worktree_id, _, _)| *worktree_id) + .map(|worktree| worktree.worktree_id) .collect(); let mut last_found: Option = None; @@ -3761,8 +3804,8 @@ impl ProjectPanel { let entries = self .visible_entries .iter() - .find(|(worktree_id, _, _)| *worktree_id == start.worktree_id) - .map(|(_, entries, _)| entries)?; + .find(|worktree| worktree.worktree_id == start.worktree_id) + .map(|worktree| &worktree.entries)?; let mut start_idx = entries .iter() @@ -4914,7 +4957,7 @@ impl ProjectPanel { let (active_indent_range, depth) = { 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 depth = entry.path.ancestors().count(); while let Some(entry) = child_paths.get(child_offset + child_count + 1) { @@ -4927,9 +4970,14 @@ impl ProjectPanel { let start = ix + 1; let end = start + child_count; - let (_, entries, paths) = &self.visible_entries[worktree_ix]; - let visible_worktree_entries = - paths.get_or_init(|| entries.iter().map(|e| (e.path.clone())).collect()); + let visible_worktree = &self.visible_entries[worktree_ix]; + let visible_worktree_entries = visible_worktree.index.get_or_init(|| { + 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. let (depth, _) = Self::calculate_depth_and_difference(entry, visible_worktree_entries); @@ -4964,10 +5012,10 @@ impl ProjectPanel { return SmallVec::new(); }; - let Some((_, visible_worktree_entries, entries_paths)) = self + let Some(visible) = self .visible_entries .iter() - .find(|(id, _, _)| *id == worktree_id) + .find(|worktree| worktree.worktree_id == worktree_id) else { return SmallVec::new(); }; @@ -4977,12 +5025,9 @@ impl ProjectPanel { }; let worktree = worktree.read(cx).snapshot(); - let paths = entries_paths.get_or_init(|| { - visible_worktree_entries - .iter() - .map(|e| e.path.clone()) - .collect() - }); + let paths = visible + .index + .get_or_init(|| visible.entries.iter().map(|e| e.path.clone()).collect()); let mut sticky_parents = Vec::new(); let mut current_path = entry_ref.path.clone(); @@ -5012,7 +5057,8 @@ impl ProjectPanel { let root_name = OsStr::new(worktree.root_name()); let git_summaries_by_id = if git_status_enabled { - visible_worktree_entries + visible + .entries .iter() .map(|e| (e.id, e.git_summary)) .collect::>() @@ -5110,7 +5156,7 @@ impl Render for ProjectPanel { let item_count = self .visible_entries .iter() - .map(|(_, worktree_entries, _)| worktree_entries.len()) + .map(|worktree| worktree.entries.len()) .sum(); fn handle_drag_move(