Speed up is_dirty and has_conflict (#11946)

I noticed that scrolling the assistant panel was very slow in debug
mode, after running a completion. From profiling, I saw that it was due
to the buffer's `is_dirty` and `has_conflict` checks, which use
`edits_since` to check if there are any non-undone edits since the saved
version.

I optimized this in two ways:
* I introduced a specialized `has_edits_since` method on text buffers,
which allows us to more cheaply check if the buffer has been edited
since a given version, without some of the overhead involved in
computing what the edits actually are.
* In the case of `has_conflict`, we don't even need to call that method
in the case where the buffer doesn't have a file (is untitled, as is the
case in the assistant panel). Buffers without files cannot be in
conflict.

Release Notes:

- Improved performance of editing the assistant panel and untitled
buffers with many edits.
This commit is contained in:
Max Brunsfeld 2024-05-16 18:36:20 -07:00 committed by GitHub
parent 23315d214c
commit df3bd40c56
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 32 additions and 11 deletions

View File

@ -1527,14 +1527,10 @@ impl Buffer {
self.end_transaction(cx)
}
fn changed_since_saved_version(&self) -> bool {
self.edits_since::<usize>(&self.saved_version)
.next()
.is_some()
}
/// Checks if the buffer has unsaved changes.
pub fn is_dirty(&self) -> bool {
(self.has_conflict || self.changed_since_saved_version())
self.has_conflict
|| self.has_edits_since(&self.saved_version)
|| self
.file
.as_ref()
@ -1544,11 +1540,10 @@ impl Buffer {
/// Checks if the buffer and its file have both changed since the buffer
/// was last saved or reloaded.
pub fn has_conflict(&self) -> bool {
(self.has_conflict || self.changed_since_saved_version())
&& self
.file
.as_ref()
.map_or(false, |file| file.mtime() > self.saved_mtime)
self.has_conflict
|| self.file.as_ref().map_or(false, |file| {
file.mtime() > self.saved_mtime && self.has_edits_since(&self.saved_version)
})
}
/// Gets a [`Subscription`] that tracks all of the changes to the buffer's text.

View File

@ -139,6 +139,14 @@ fn test_random_edits(mut rng: StdRng) {
assert_eq!(old_text, new_text);
}
assert_eq!(
buffer.has_edits_since(&old_buffer.version),
buffer
.edits_since::<usize>(&old_buffer.version)
.next()
.is_some(),
);
let subscription_edits = subscription.consume();
log::info!(
"applying subscription edits since version {:?} to old text: {:?}: {:?}",

View File

@ -2165,6 +2165,24 @@ impl BufferSnapshot {
buffer_id: self.remote_id,
}
}
pub fn has_edits_since(&self, since: &clock::Global) -> bool {
if *since != self.version {
let mut cursor = self
.fragments
.filter::<_, usize>(move |summary| !since.observed_all(&summary.max_version));
cursor.next(&None);
while let Some(fragment) = cursor.item() {
let was_visible = fragment.was_visible(since, &self.undo_map);
let is_visible = fragment.visible;
if was_visible != is_visible {
return true;
}
cursor.next(&None);
}
}
false
}
}
struct RopeBuilder<'a> {