Track dirtyness per item (#26237)

This reduces the number of multibuffer syncs when starting the editor
with 80
files open in the Zed repo from 10,000,000 to 100,000 by avoiding
O(n**2)
dirtyness checks.

Release Notes:

- Fixed a beachball when restarting in a large repo with a large number
open files
This commit is contained in:
Conrad Irwin 2025-03-06 15:12:56 -07:00 committed by GitHub
parent 263d9ff755
commit f373383fc1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 43 additions and 7 deletions

View file

@ -802,6 +802,7 @@ impl<T: Item> ItemHandle for Entity<T> {
}
ItemEvent::UpdateTab => {
workspace.update_item_dirty_state(item, window, cx);
pane.update(cx, |_, cx| {
cx.emit(pane::Event::ChangeItemTitle);
cx.notify();

View file

@ -826,6 +826,7 @@ pub struct Workspace {
follower_states: HashMap<PeerId, FollowerState>,
last_leaders_by_pane: HashMap<WeakEntity<Pane>, PeerId>,
window_edited: bool,
dirty_items: HashMap<EntityId, Subscription>,
active_call: Option<(Entity<ActiveCall>, Vec<Subscription>)>,
leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
database_id: Option<WorkspaceId>,
@ -1128,6 +1129,7 @@ impl Workspace {
last_leaders_by_pane: Default::default(),
dispatching_keystrokes: Default::default(),
window_edited: false,
dirty_items: Default::default(),
active_call,
database_id: workspace_id,
app_state,
@ -3395,13 +3397,11 @@ impl Workspace {
if *pane == self.active_pane {
self.active_item_path_changed(window, cx);
}
self.update_window_edited(window, cx);
serialize_workspace = false;
}
pane::Event::RemoveItem { .. } => {}
pane::Event::RemovedItem { item_id } => {
cx.emit(Event::ActiveItemChanged);
self.update_window_edited(window, cx);
if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(*item_id) {
if entry.get().entity_id() == pane.entity_id() {
entry.remove();
@ -3858,16 +3858,47 @@ impl Workspace {
}
fn update_window_edited(&mut self, window: &mut Window, cx: &mut App) {
let is_edited = !self.project.read(cx).is_disconnected(cx)
&& self
.items(cx)
.any(|item| item.has_conflict(cx) || item.is_dirty(cx));
let is_edited = !self.project.read(cx).is_disconnected(cx) && !self.dirty_items.is_empty();
if is_edited != self.window_edited {
self.window_edited = is_edited;
window.set_window_edited(self.window_edited)
}
}
fn update_item_dirty_state(
&mut self,
item: &dyn ItemHandle,
window: &mut Window,
cx: &mut App,
) {
let is_dirty = item.is_dirty(cx);
let item_id = item.item_id();
let was_dirty = self.dirty_items.contains_key(&item_id);
if is_dirty == was_dirty {
return;
}
if was_dirty {
self.dirty_items.remove(&item_id);
self.update_window_edited(window, cx);
return;
}
if let Some(window_handle) = window.window_handle().downcast::<Self>() {
let s = item.on_release(
cx,
Box::new(move |cx| {
window_handle
.update(cx, |this, window, cx| {
this.dirty_items.remove(&item_id);
this.update_window_edited(window, cx)
})
.ok();
}),
);
self.dirty_items.insert(item_id, s);
self.update_window_edited(window, cx);
}
}
fn render_notifications(&self, _window: &mut Window, _cx: &mut Context<Self>) -> Option<Div> {
if self.notifications.is_empty() {
None

View file

@ -2112,6 +2112,7 @@ mod tests {
})
.unwrap();
assert!(window_is_edited(window, cx));
let weak = editor.downgrade();
// Closing the item restores the window's edited state.
let close = window
@ -2127,11 +2128,12 @@ mod tests {
cx.simulate_prompt_answer("Don't Save");
close.await.unwrap();
assert!(!window_is_edited(window, cx));
// Advance the clock to ensure that the item has been serialized and dropped from the queue
cx.executor().advance_clock(Duration::from_secs(1));
weak.assert_released();
assert!(!window_is_edited(window, cx));
// Opening the buffer again doesn't impact the window's edited state.
cx.update(|cx| {
open_paths(
@ -2266,6 +2268,8 @@ mod tests {
assert_eq!(cx.update(|cx| cx.windows().len()), 1);
assert!(cx.update(|cx| cx.active_window().is_some()));
cx.run_until_parked();
// When opening the workspace, the window is not in a edited state.
let window = cx.update(|cx| cx.active_window().unwrap().downcast::<Workspace>().unwrap());
assert!(window_is_edited(window, cx));