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 serde::{Deserialize, Serialize};
use std::{
cell::OnceCell,
collections::HashSet,
ffi::OsStr,
ops::Range,
@ -46,7 +47,7 @@ pub struct ProjectPanel {
fs: Arc<dyn Fs>,
scroll_handle: UniformListScrollHandle,
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_external_paths_drag_over_entry: Option<ProjectEntryId>,
expanded_dir_ids: HashMap<WorktreeId, Vec<ProjectEntryId>>,
@ -675,7 +676,7 @@ impl ProjectPanel {
return;
}
let (worktree_id, worktree_entries) = &self.visible_entries[worktree_ix];
let (worktree_id, worktree_entries, _) = &self.visible_entries[worktree_ix];
let selection = SelectedEntry {
worktree_id: *worktree_id,
entry_id: worktree_entries[entry_ix].id,
@ -1120,7 +1121,7 @@ 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) {
if entry_ix + 1 < worktree_entries.len() {
entry_ix += 1;
} 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) {
let selection = SelectedEntry {
worktree_id: *worktree_id,
@ -1170,7 +1172,9 @@ impl ProjectPanel {
let worktree = self
.visible_entries
.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 {
let worktree = worktree.read(cx);
let worktree_id = worktree.id();
@ -1190,10 +1194,9 @@ impl ProjectPanel {
}
fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
let worktree = self
.visible_entries
.last()
.and_then(|(worktree_id, _)| self.project.read(cx).worktree_for_id(*worktree_id, cx));
let worktree = self.visible_entries.last().and_then(|(worktree_id, _, _)| {
self.project.read(cx).worktree_for_id(*worktree_id, cx)
});
if let Some(worktree) = worktree {
let worktree = worktree.read(cx);
let worktree_id = worktree.id();
@ -1461,7 +1464,7 @@ impl ProjectPanel {
fn index_for_selection(&self, selection: SelectedEntry) -> Option<(usize, usize, usize)> {
let mut entry_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()
{
if *worktree_id == selection.worktree_id {
@ -1623,7 +1626,7 @@ impl ProjectPanel {
snapshot.propagate_git_statuses(&mut visible_worktree_entries);
project::sort_worktree_entries(&mut visible_worktree_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 {
@ -1794,7 +1797,7 @@ impl ProjectPanel {
mut callback: impl FnMut(ProjectEntryId, EntryDetails, &mut ViewContext<ProjectPanel>),
) {
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 {
return;
}
@ -1823,10 +1826,12 @@ impl ProjectPanel {
.unwrap_or(&[]);
let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
let entries = visible_worktree_entries
.iter()
.map(|e| (e.path.clone()))
.collect();
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 status = git_status_setting.then(|| entry.git_status).flatten();
let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok();
@ -2298,7 +2303,7 @@ impl Render for ProjectPanel {
"entries",
self.visible_entries
.iter()
.map(|(_, worktree_entries)| worktree_entries.len())
.map(|(_, worktree_entries, _)| worktree_entries.len())
.sum(),
{
|this, range, cx| {