Fix performance bottlenecks when multi buffers have huge numbers of buffers (#26308)

This is motivated by trying to make the Project Diff view usable with
huge Git change sets.

Release Notes:

- Improved performance of rendering multibuffers with very large numbers
of buffers
This commit is contained in:
Max Brunsfeld 2025-03-07 18:15:15 -08:00 committed by GitHub
parent cb543f9546
commit 4846e6fb3a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 62 additions and 21 deletions

View file

@ -49,7 +49,7 @@ use std::{
num::NonZeroU32, num::NonZeroU32,
ops::{Deref, DerefMut, Range}, ops::{Deref, DerefMut, Range},
path::{Path, PathBuf}, path::{Path, PathBuf},
str, rc, str,
sync::{Arc, LazyLock}, sync::{Arc, LazyLock},
time::{Duration, Instant}, time::{Duration, Instant},
vec, vec,
@ -125,6 +125,7 @@ pub struct Buffer {
/// Memoize calls to has_changes_since(saved_version). /// Memoize calls to has_changes_since(saved_version).
/// The contents of a cell are (self.version, has_changes) at the time of a last call. /// The contents of a cell are (self.version, has_changes) at the time of a last call.
has_unsaved_edits: Cell<(clock::Global, bool)>, has_unsaved_edits: Cell<(clock::Global, bool)>,
change_bits: Vec<rc::Weak<Cell<bool>>>,
_subscriptions: Vec<gpui::Subscription>, _subscriptions: Vec<gpui::Subscription>,
} }
@ -978,6 +979,7 @@ impl Buffer {
completion_triggers_timestamp: Default::default(), completion_triggers_timestamp: Default::default(),
deferred_ops: OperationQueue::new(), deferred_ops: OperationQueue::new(),
has_conflict: false, has_conflict: false,
change_bits: Default::default(),
_subscriptions: Vec::new(), _subscriptions: Vec::new(),
} }
} }
@ -1252,6 +1254,7 @@ impl Buffer {
self.non_text_state_update_count += 1; self.non_text_state_update_count += 1;
self.syntax_map.lock().clear(&self.text); self.syntax_map.lock().clear(&self.text);
self.language = language; self.language = language;
self.was_changed();
self.reparse(cx); self.reparse(cx);
cx.emit(BufferEvent::LanguageChanged); cx.emit(BufferEvent::LanguageChanged);
} }
@ -1286,6 +1289,7 @@ impl Buffer {
.set((self.saved_version().clone(), false)); .set((self.saved_version().clone(), false));
self.has_conflict = false; self.has_conflict = false;
self.saved_mtime = mtime; self.saved_mtime = mtime;
self.was_changed();
cx.emit(BufferEvent::Saved); cx.emit(BufferEvent::Saved);
cx.notify(); cx.notify();
} }
@ -1381,6 +1385,7 @@ impl Buffer {
self.file = Some(new_file); self.file = Some(new_file);
if file_changed { if file_changed {
self.was_changed();
self.non_text_state_update_count += 1; self.non_text_state_update_count += 1;
if was_dirty != self.is_dirty() { if was_dirty != self.is_dirty() {
cx.emit(BufferEvent::DirtyChanged); cx.emit(BufferEvent::DirtyChanged);
@ -1958,6 +1963,23 @@ impl Buffer {
self.text.subscribe() self.text.subscribe()
} }
/// Adds a bit to the list of bits that are set when the buffer's text changes.
///
/// This allows downstream code to check if the buffer's text has changed without
/// waiting for an effect cycle, which would be required if using eents.
pub fn record_changes(&mut self, bit: rc::Weak<Cell<bool>>) {
self.change_bits.push(bit);
}
fn was_changed(&mut self) {
self.change_bits.retain(|change_bit| {
change_bit.upgrade().map_or(false, |bit| {
bit.replace(true);
true
})
});
}
/// Starts a transaction, if one is not already in-progress. When undoing or /// Starts a transaction, if one is not already in-progress. When undoing or
/// redoing edits, all of the edits performed within a transaction are undone /// redoing edits, all of the edits performed within a transaction are undone
/// or redone together. /// or redone together.
@ -2368,6 +2390,7 @@ impl Buffer {
} }
self.text.apply_ops(buffer_ops); self.text.apply_ops(buffer_ops);
self.deferred_ops.insert(deferred_ops); self.deferred_ops.insert(deferred_ops);
self.was_changed();
self.flush_deferred_ops(cx); self.flush_deferred_ops(cx);
self.did_edit(&old_version, was_dirty, cx); self.did_edit(&old_version, was_dirty, cx);
// Notify independently of whether the buffer was edited as the operations could include a // Notify independently of whether the buffer was edited as the operations could include a
@ -2502,7 +2525,8 @@ impl Buffer {
} }
} }
fn send_operation(&self, operation: Operation, is_local: bool, cx: &mut Context<Self>) { fn send_operation(&mut self, operation: Operation, is_local: bool, cx: &mut Context<Self>) {
self.was_changed();
cx.emit(BufferEvent::Operation { cx.emit(BufferEvent::Operation {
operation, operation,
is_local, is_local,

View file

@ -31,7 +31,7 @@ use smol::future::yield_now;
use std::{ use std::{
any::type_name, any::type_name,
borrow::Cow, borrow::Cow,
cell::{Ref, RefCell}, cell::{Cell, Ref, RefCell},
cmp, fmt, cmp, fmt,
future::Future, future::Future,
io, io,
@ -39,6 +39,7 @@ use std::{
mem, mem,
ops::{Range, RangeBounds, Sub}, ops::{Range, RangeBounds, Sub},
path::Path, path::Path,
rc::Rc,
str, str,
sync::Arc, sync::Arc,
time::{Duration, Instant}, time::{Duration, Instant},
@ -76,6 +77,7 @@ pub struct MultiBuffer {
history: History, history: History,
title: Option<String>, title: Option<String>,
capability: Capability, capability: Capability,
buffer_changed_since_sync: Rc<Cell<bool>>,
} }
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
@ -568,6 +570,7 @@ impl MultiBuffer {
capability, capability,
title: None, title: None,
buffers_by_path: Default::default(), buffers_by_path: Default::default(),
buffer_changed_since_sync: Default::default(),
history: History { history: History {
next_transaction_id: clock::Lamport::default(), next_transaction_id: clock::Lamport::default(),
undo_stack: Vec::new(), undo_stack: Vec::new(),
@ -587,6 +590,7 @@ impl MultiBuffer {
subscriptions: Default::default(), subscriptions: Default::default(),
singleton: false, singleton: false,
capability, capability,
buffer_changed_since_sync: Default::default(),
history: History { history: History {
next_transaction_id: Default::default(), next_transaction_id: Default::default(),
undo_stack: Default::default(), undo_stack: Default::default(),
@ -600,7 +604,11 @@ impl MultiBuffer {
pub fn clone(&self, new_cx: &mut Context<Self>) -> Self { pub fn clone(&self, new_cx: &mut Context<Self>) -> Self {
let mut buffers = HashMap::default(); let mut buffers = HashMap::default();
let buffer_changed_since_sync = Rc::new(Cell::new(false));
for (buffer_id, buffer_state) in self.buffers.borrow().iter() { for (buffer_id, buffer_state) in self.buffers.borrow().iter() {
buffer_state.buffer.update(new_cx, |buffer, _| {
buffer.record_changes(Rc::downgrade(&buffer_changed_since_sync));
});
buffers.insert( buffers.insert(
*buffer_id, *buffer_id,
BufferState { BufferState {
@ -629,6 +637,7 @@ impl MultiBuffer {
capability: self.capability, capability: self.capability,
history: self.history.clone(), history: self.history.clone(),
title: self.title.clone(), title: self.title.clone(),
buffer_changed_since_sync,
} }
} }
@ -1728,19 +1737,25 @@ impl MultiBuffer {
self.sync(cx); self.sync(cx);
let buffer_id = buffer.read(cx).remote_id();
let buffer_snapshot = buffer.read(cx).snapshot(); let buffer_snapshot = buffer.read(cx).snapshot();
let buffer_id = buffer_snapshot.remote_id();
let mut buffers = self.buffers.borrow_mut(); let mut buffers = self.buffers.borrow_mut();
let buffer_state = buffers.entry(buffer_id).or_insert_with(|| BufferState { let buffer_state = buffers.entry(buffer_id).or_insert_with(|| {
last_version: buffer_snapshot.version().clone(), self.buffer_changed_since_sync.replace(true);
last_non_text_state_update_count: buffer_snapshot.non_text_state_update_count(), buffer.update(cx, |buffer, _| {
excerpts: Default::default(), buffer.record_changes(Rc::downgrade(&self.buffer_changed_since_sync));
_subscriptions: [ });
cx.observe(&buffer, |_, _, cx| cx.notify()), BufferState {
cx.subscribe(&buffer, Self::on_buffer_event), last_version: buffer_snapshot.version().clone(),
], last_non_text_state_update_count: buffer_snapshot.non_text_state_update_count(),
buffer: buffer.clone(), excerpts: Default::default(),
_subscriptions: [
cx.observe(&buffer, |_, _, cx| cx.notify()),
cx.subscribe(&buffer, Self::on_buffer_event),
],
buffer: buffer.clone(),
}
}); });
let mut snapshot = self.snapshot.borrow_mut(); let mut snapshot = self.snapshot.borrow_mut();
@ -2236,6 +2251,7 @@ impl MultiBuffer {
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
self.sync(cx); self.sync(cx);
self.buffer_changed_since_sync.replace(true);
let diff = diff.read(cx); let diff = diff.read(cx);
let buffer_id = diff.buffer_id; let buffer_id = diff.buffer_id;
@ -2714,6 +2730,11 @@ impl MultiBuffer {
} }
fn sync(&self, cx: &App) { fn sync(&self, cx: &App) {
let changed = self.buffer_changed_since_sync.replace(false);
if !changed {
return;
}
let mut snapshot = self.snapshot.borrow_mut(); let mut snapshot = self.snapshot.borrow_mut();
let mut excerpts_to_edit = Vec::new(); let mut excerpts_to_edit = Vec::new();
let mut non_text_state_updated = false; let mut non_text_state_updated = false;

View file

@ -2424,14 +2424,10 @@ impl Pane {
.child(label), .child(label),
); );
let single_entry_to_resolve = { let single_entry_to_resolve = self.items[ix]
let item_entries = self.items[ix].project_entry_ids(cx); .is_singleton(cx)
if item_entries.len() == 1 { .then(|| self.items[ix].project_entry_ids(cx).get(0).copied())
Some(item_entries[0]) .flatten();
} else {
None
}
};
let total_items = self.items.len(); let total_items = self.items.len();
let has_items_to_left = ix > 0; let has_items_to_left = ix > 0;