project panel: Improve performance in large projects (#13202)

In #12980 I've hoisted out creation of HashSet<PathInWorktree> out of
render_entry, which made us not create that hash set for each entry in a
worktree on each frame. In current nightly, we do it once per call to
render() on the whole worktree, which is better.

However, we can still reuse the hashed between the frames, if the
worktree has not changed. Once we calculate the hashset for a given
worktree state, we keep it around for as long as the state is valid for.
We calculate the HashSet lazily, as we may not necessarily need it if
the project panel is collapsed. In large worktrees, this helps keep the
CPU usage of the main thread low-ish.


Release Notes:

- Improved performance of project panel in large worktrees.
This commit is contained in:
Piotr Osiewicz 2024-06-18 15:09:52 +02:00 committed by GitHub
parent e4ba336971
commit 5dc54863a4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -22,6 +22,7 @@ use project::{Entry, EntryKind, Fs, Project, ProjectEntryId, ProjectPath, Worktr
use project_panel_settings::{ProjectPanelDockPosition, ProjectPanelSettings}; use project_panel_settings::{ProjectPanelDockPosition, ProjectPanelSettings};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{ use std::{
cell::OnceCell,
collections::HashSet, collections::HashSet,
ffi::OsStr, ffi::OsStr,
ops::Range, ops::Range,
@ -46,7 +47,7 @@ pub struct ProjectPanel {
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
scroll_handle: UniformListScrollHandle, scroll_handle: UniformListScrollHandle,
focus_handle: FocusHandle, focus_handle: FocusHandle,
visible_entries: Vec<(WorktreeId, Vec<Entry>)>, visible_entries: Vec<(WorktreeId, Vec<Entry>, OnceCell<HashSet<Arc<Path>>>)>,
last_worktree_root_id: Option<ProjectEntryId>, last_worktree_root_id: Option<ProjectEntryId>,
last_external_paths_drag_over_entry: Option<ProjectEntryId>, last_external_paths_drag_over_entry: Option<ProjectEntryId>,
expanded_dir_ids: HashMap<WorktreeId, Vec<ProjectEntryId>>, expanded_dir_ids: HashMap<WorktreeId, Vec<ProjectEntryId>>,
@ -675,7 +676,7 @@ impl ProjectPanel {
return; return;
} }
let (worktree_id, worktree_entries) = &self.visible_entries[worktree_ix]; let (worktree_id, worktree_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: worktree_entries[entry_ix].id,
@ -1120,7 +1121,7 @@ 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) {
if entry_ix + 1 < worktree_entries.len() { if entry_ix + 1 < worktree_entries.len() {
entry_ix += 1; entry_ix += 1;
} else { } else {
@ -1129,7 +1130,8 @@ impl ProjectPanel {
} }
} }
if let Some((worktree_id, worktree_entries)) = self.visible_entries.get(worktree_ix) { if let Some((worktree_id, worktree_entries, _)) = self.visible_entries.get(worktree_ix)
{
if let Some(entry) = worktree_entries.get(entry_ix) { if let Some(entry) = worktree_entries.get(entry_ix) {
let selection = SelectedEntry { let selection = SelectedEntry {
worktree_id: *worktree_id, worktree_id: *worktree_id,
@ -1170,7 +1172,9 @@ impl ProjectPanel {
let worktree = self let worktree = self
.visible_entries .visible_entries
.first() .first()
.and_then(|(worktree_id, _)| self.project.read(cx).worktree_for_id(*worktree_id, cx)); .and_then(|(worktree_id, _, _)| {
self.project.read(cx).worktree_for_id(*worktree_id, cx)
});
if let Some(worktree) = worktree { if let Some(worktree) = worktree {
let worktree = worktree.read(cx); let worktree = worktree.read(cx);
let worktree_id = worktree.id(); let worktree_id = worktree.id();
@ -1190,10 +1194,9 @@ impl ProjectPanel {
} }
fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) { fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
let worktree = self let worktree = self.visible_entries.last().and_then(|(worktree_id, _, _)| {
.visible_entries self.project.read(cx).worktree_for_id(*worktree_id, cx)
.last() });
.and_then(|(worktree_id, _)| self.project.read(cx).worktree_for_id(*worktree_id, cx));
if let Some(worktree) = worktree { if let Some(worktree) = worktree {
let worktree = worktree.read(cx); let worktree = worktree.read(cx);
let worktree_id = worktree.id(); let worktree_id = worktree.id();
@ -1461,7 +1464,7 @@ impl ProjectPanel {
fn index_for_selection(&self, selection: SelectedEntry) -> Option<(usize, usize, usize)> { fn index_for_selection(&self, selection: SelectedEntry) -> Option<(usize, usize, usize)> {
let mut entry_index = 0; let mut entry_index = 0;
let mut visible_entries_index = 0; let mut visible_entries_index = 0;
for (worktree_index, (worktree_id, worktree_entries)) in for (worktree_index, (worktree_id, worktree_entries, _)) in
self.visible_entries.iter().enumerate() self.visible_entries.iter().enumerate()
{ {
if *worktree_id == selection.worktree_id { if *worktree_id == selection.worktree_id {
@ -1623,7 +1626,7 @@ impl ProjectPanel {
snapshot.propagate_git_statuses(&mut visible_worktree_entries); snapshot.propagate_git_statuses(&mut visible_worktree_entries);
project::sort_worktree_entries(&mut visible_worktree_entries); project::sort_worktree_entries(&mut visible_worktree_entries);
self.visible_entries self.visible_entries
.push((worktree_id, visible_worktree_entries)); .push((worktree_id, visible_worktree_entries, OnceCell::new()));
} }
if let Some((worktree_id, entry_id)) = new_selected_entry { if let Some((worktree_id, entry_id)) = new_selected_entry {
@ -1794,7 +1797,7 @@ impl ProjectPanel {
mut callback: impl FnMut(ProjectEntryId, EntryDetails, &mut ViewContext<ProjectPanel>), mut callback: impl FnMut(ProjectEntryId, EntryDetails, &mut ViewContext<ProjectPanel>),
) { ) {
let mut ix = 0; let mut ix = 0;
for (worktree_id, visible_worktree_entries) in &self.visible_entries { for (worktree_id, visible_worktree_entries, entries_paths) in &self.visible_entries {
if ix >= range.end { if ix >= range.end {
return; return;
} }
@ -1823,10 +1826,12 @@ impl ProjectPanel {
.unwrap_or(&[]); .unwrap_or(&[]);
let entry_range = range.start.saturating_sub(ix)..end_ix - ix; let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
let entries = visible_worktree_entries let entries = entries_paths.get_or_init(|| {
.iter() visible_worktree_entries
.map(|e| (e.path.clone())) .iter()
.collect(); .map(|e| (e.path.clone()))
.collect()
});
for entry in visible_worktree_entries[entry_range].iter() { for entry in visible_worktree_entries[entry_range].iter() {
let status = git_status_setting.then(|| entry.git_status).flatten(); let status = git_status_setting.then(|| entry.git_status).flatten();
let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok(); let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok();
@ -2298,7 +2303,7 @@ impl Render for ProjectPanel {
"entries", "entries",
self.visible_entries self.visible_entries
.iter() .iter()
.map(|(_, worktree_entries)| worktree_entries.len()) .map(|(_, worktree_entries, _)| worktree_entries.len())
.sum(), .sum(),
{ {
|this, range, cx| { |this, range, cx| {