From ac3b232dda334199c800a382a809badef52d7f19 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Thu, 30 Oct 2025 18:40:56 +0100 Subject: [PATCH] Reduce amount of foreground tasks spawned on multibuffer/editor updates (#41479) When doing a project wide search in zed on windows for `hang`, zed starts to freeze for a couple seconds ultimately starting to error with `Not enough quota is available to process this command.` when dispatching windows messages. The cause for this is that we simply overload the windows message pump due to the sheer amount of foreground tasks we spawn when we populate the project search. This PR is an attempt at reducing this. Release Notes: - Reduced hangs and stutters in large project file searches --- crates/editor/src/display_map/wrap_map.rs | 7 +--- crates/editor/src/git/blame.rs | 1 + crates/go_to_line/src/cursor_position.rs | 32 ++++++++-------- crates/gpui/src/app/async_context.rs | 2 +- crates/gpui/src/executor.rs | 1 - crates/language/src/buffer.rs | 25 ++++++------ crates/multi_buffer/src/path_key.rs | 16 ++++---- crates/project/src/buffer_store.rs | 46 +++++++++++------------ crates/project/src/git_store.rs | 1 + crates/project/src/project_tests.rs | 4 +- crates/search/src/project_search.rs | 30 +++++++++------ crates/worktree/src/worktree.rs | 4 +- crates/zed/src/zed.rs | 18 +++++---- 13 files changed, 101 insertions(+), 86 deletions(-) diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index e79e5555a6..7371eb6785 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -568,18 +568,15 @@ impl WrapSnapshot { let mut old_start = old_cursor.start().output.lines; old_start += tab_edit.old.start.0 - old_cursor.start().input.lines; - // todo(lw): Should these be seek_forward? - old_cursor.seek(&tab_edit.old.end, Bias::Right); + old_cursor.seek_forward(&tab_edit.old.end, Bias::Right); let mut old_end = old_cursor.start().output.lines; old_end += tab_edit.old.end.0 - old_cursor.start().input.lines; - // todo(lw): Should these be seek_forward? new_cursor.seek(&tab_edit.new.start, Bias::Right); let mut new_start = new_cursor.start().output.lines; new_start += tab_edit.new.start.0 - new_cursor.start().input.lines; - // todo(lw): Should these be seek_forward? - new_cursor.seek(&tab_edit.new.end, Bias::Right); + new_cursor.seek_forward(&tab_edit.new.end, Bias::Right); let mut new_end = new_cursor.start().output.lines; new_end += tab_edit.new.end.0 - new_cursor.start().input.lines; diff --git a/crates/editor/src/git/blame.rs b/crates/editor/src/git/blame.rs index 4f210cc9db..b36a57a7e4 100644 --- a/crates/editor/src/git/blame.rs +++ b/crates/editor/src/git/blame.rs @@ -602,6 +602,7 @@ impl GitBlame { } fn regenerate_on_edit(&mut self, cx: &mut Context) { + // todo(lw): hot foreground spawn self.regenerate_on_edit_task = cx.spawn(async move |this, cx| { cx.background_executor() .timer(REGENERATE_ON_EDIT_DEBOUNCE_INTERVAL) diff --git a/crates/go_to_line/src/cursor_position.rs b/crates/go_to_line/src/cursor_position.rs index 2638a49eba..2a67ff6747 100644 --- a/crates/go_to_line/src/cursor_position.rs +++ b/crates/go_to_line/src/cursor_position.rs @@ -1,4 +1,4 @@ -use editor::{Editor, MultiBufferSnapshot}; +use editor::{Editor, EditorEvent, MultiBufferSnapshot}; use gpui::{App, Entity, FocusHandle, Focusable, Styled, Subscription, Task, WeakEntity}; use settings::Settings; use std::{fmt::Write, num::NonZeroU32, time::Duration}; @@ -81,7 +81,7 @@ impl CursorPosition { fn update_position( &mut self, - editor: Entity, + editor: &Entity, debounce: Option, window: &mut Window, cx: &mut Context, @@ -269,19 +269,21 @@ impl StatusItemView for CursorPosition { cx: &mut Context, ) { if let Some(editor) = active_pane_item.and_then(|item| item.act_as::(cx)) { - self._observe_active_editor = - Some( - cx.observe_in(&editor, window, |cursor_position, editor, window, cx| { - Self::update_position( - cursor_position, - editor, - Some(UPDATE_DEBOUNCE), - window, - cx, - ) - }), - ); - self.update_position(editor, None, window, cx); + self._observe_active_editor = Some(cx.subscribe_in( + &editor, + window, + |cursor_position, editor, event, window, cx| match event { + EditorEvent::SelectionsChanged { .. } => Self::update_position( + cursor_position, + editor, + Some(UPDATE_DEBOUNCE), + window, + cx, + ), + _ => {} + }, + )); + self.update_position(&editor, None, window, cx); } else { self.position = None; self._observe_active_editor = None; diff --git a/crates/gpui/src/app/async_context.rs b/crates/gpui/src/app/async_context.rs index cfe7a5a75c..381541d4b1 100644 --- a/crates/gpui/src/app/async_context.rs +++ b/crates/gpui/src/app/async_context.rs @@ -176,7 +176,7 @@ impl AsyncApp { lock.open_window(options, build_root_view) } - /// Schedule a future to be polled in the background. + /// Schedule a future to be polled in the foreground. #[track_caller] pub fn spawn(&self, f: AsyncFn) -> Task where diff --git a/crates/gpui/src/executor.rs b/crates/gpui/src/executor.rs index b820e120dd..b6d3a407f5 100644 --- a/crates/gpui/src/executor.rs +++ b/crates/gpui/src/executor.rs @@ -479,7 +479,6 @@ impl ForegroundExecutor { } /// Enqueues the given Task to run on the main thread at some point in the future. - #[track_caller] pub fn spawn(&self, future: impl Future + 'static) -> Task where R: 'static, diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index c2da93aa73..c72350f385 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1573,21 +1573,24 @@ impl Buffer { self.reparse = None; } Err(parse_task) => { + // todo(lw): hot foreground spawn self.reparse = Some(cx.spawn(async move |this, cx| { - let new_syntax_map = parse_task.await; + let new_syntax_map = cx.background_spawn(parse_task).await; this.update(cx, move |this, cx| { - let grammar_changed = + let grammar_changed = || { this.language.as_ref().is_none_or(|current_language| { !Arc::ptr_eq(&language, current_language) - }); - let language_registry_changed = new_syntax_map - .contains_unknown_injections() - && language_registry.is_some_and(|registry| { - registry.version() != new_syntax_map.language_registry_version() - }); - let parse_again = language_registry_changed - || grammar_changed - || this.version.changed_since(&parsed_version); + }) + }; + let language_registry_changed = || { + new_syntax_map.contains_unknown_injections() + && language_registry.is_some_and(|registry| { + registry.version() != new_syntax_map.language_registry_version() + }) + }; + let parse_again = this.version.changed_since(&parsed_version) + || language_registry_changed() + || grammar_changed(); this.did_finish_parsing(new_syntax_map, cx); this.reparse = None; if parse_again { diff --git a/crates/multi_buffer/src/path_key.rs b/crates/multi_buffer/src/path_key.rs index b6175b7aaa..568d1ac867 100644 --- a/crates/multi_buffer/src/path_key.rs +++ b/crates/multi_buffer/src/path_key.rs @@ -1,7 +1,7 @@ use std::{mem, ops::Range, sync::Arc}; use collections::HashSet; -use gpui::{App, AppContext, Context, Entity, Task}; +use gpui::{App, AppContext, Context, Entity}; use itertools::Itertools; use language::{Buffer, BufferSnapshot}; use rope::Point; @@ -117,12 +117,14 @@ impl MultiBuffer { buffer: Entity, ranges: Vec>, context_line_count: u32, - cx: &mut Context, - ) -> Task>> { + cx: &Context, + ) -> impl Future>> + use<> { let buffer_snapshot = buffer.read(cx).snapshot(); - cx.spawn(async move |multi_buffer, cx| { + let multi_buffer = cx.weak_entity(); + let mut app = cx.to_async(); + async move { let snapshot = buffer_snapshot.clone(); - let (excerpt_ranges, new, counts) = cx + let (excerpt_ranges, new, counts) = app .background_spawn(async move { let ranges = ranges.into_iter().map(|range| range.to_point(&snapshot)); let excerpt_ranges = @@ -133,7 +135,7 @@ impl MultiBuffer { .await; multi_buffer - .update(cx, move |multi_buffer, cx| { + .update(&mut app, move |multi_buffer, cx| { let (ranges, _) = multi_buffer.set_merged_excerpt_ranges_for_path( path_key, buffer, @@ -147,7 +149,7 @@ impl MultiBuffer { }) .ok() .unwrap_or_default() - }) + } } pub(super) fn expand_excerpts_with_paths( diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index b9249d36e2..39e302a2d9 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -619,29 +619,24 @@ impl LocalBufferStore { worktree: Entity, cx: &mut Context, ) -> Task>> { - let load_buffer = worktree.update(cx, |worktree, cx| { - let load_file = worktree.load_file(path.as_ref(), cx); - let reservation = cx.reserve_entity(); - let buffer_id = BufferId::from(reservation.entity_id().as_non_zero_u64()); - let path = path.clone(); - cx.spawn(async move |_, cx| { - let loaded = load_file.await.with_context(|| { - format!("Could not open path: {}", path.display(PathStyle::local())) - })?; - let text_buffer = cx - .background_spawn(async move { - text::Buffer::new(ReplicaId::LOCAL, buffer_id, loaded.text) - }) - .await; - cx.insert_entity(reservation, |_| { - Buffer::build(text_buffer, Some(loaded.file), Capability::ReadWrite) - }) - }) - }); - + let load_file = worktree.update(cx, |worktree, cx| worktree.load_file(path.as_ref(), cx)); cx.spawn(async move |this, cx| { - let buffer = match load_buffer.await { - Ok(buffer) => Ok(buffer), + let path = path.clone(); + let buffer = match load_file.await.with_context(|| { + format!("Could not open path: {}", path.display(PathStyle::local())) + }) { + Ok(loaded) => { + let reservation = cx.reserve_entity::()?; + let buffer_id = BufferId::from(reservation.entity_id().as_non_zero_u64()); + let text_buffer = cx + .background_spawn(async move { + text::Buffer::new(ReplicaId::LOCAL, buffer_id, loaded.text) + }) + .await; + cx.insert_entity(reservation, |_| { + Buffer::build(text_buffer, Some(loaded.file), Capability::ReadWrite) + })? + } Err(error) if is_not_found_error(&error) => cx.new(|cx| { let buffer_id = BufferId::from(cx.entity_id().as_non_zero_u64()); let text_buffer = text::Buffer::new(ReplicaId::LOCAL, buffer_id, ""); @@ -657,9 +652,9 @@ impl LocalBufferStore { })), Capability::ReadWrite, ) - }), - Err(e) => Err(e), - }?; + })?, + Err(e) => return Err(e), + }; this.update(cx, |this, cx| { this.add_buffer(buffer.clone(), cx)?; let buffer_id = buffer.read(cx).remote_id(); @@ -840,6 +835,7 @@ impl BufferStore { entry .insert( + // todo(lw): hot foreground spawn cx.spawn(async move |this, cx| { let load_result = load_buffer.await; this.update(cx, |this, cx| { diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index 736c96f34e..03642df3b4 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -709,6 +709,7 @@ impl GitStore { repo.load_committed_text(buffer_id, repo_path, cx) }); + // todo(lw): hot foreground spawn cx.spawn(async move |this, cx| { Self::open_diff_internal(this, DiffKind::Uncommitted, changes.await, buffer, cx) .await diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 1d4dbc6c86..891ad2420c 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -9171,7 +9171,9 @@ async fn test_odd_events_for_ignored_dirs( repository_updates.lock().drain(..).collect::>(), vec![ RepositoryEvent::MergeHeadsChanged, - RepositoryEvent::BranchChanged + RepositoryEvent::BranchChanged, + RepositoryEvent::StatusesChanged { full_scan: false }, + RepositoryEvent::StatusesChanged { full_scan: false }, ], "Initial worktree scan should produce a repo update event" ); diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index f407a0a4db..a8be82d5d5 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -322,18 +322,25 @@ impl ProjectSearch { let mut limit_reached = false; while let Some(results) = matches.next().await { - let mut buffers_with_ranges = Vec::with_capacity(results.len()); - for result in results { - match result { - project::search::SearchResult::Buffer { buffer, ranges } => { - buffers_with_ranges.push((buffer, ranges)); + let (buffers_with_ranges, has_reached_limit) = cx + .background_executor() + .spawn(async move { + let mut limit_reached = false; + let mut buffers_with_ranges = Vec::with_capacity(results.len()); + for result in results { + match result { + project::search::SearchResult::Buffer { buffer, ranges } => { + buffers_with_ranges.push((buffer, ranges)); + } + project::search::SearchResult::LimitReached => { + limit_reached = true; + } + } } - project::search::SearchResult::LimitReached => { - limit_reached = true; - } - } - } - + (buffers_with_ranges, limit_reached) + }) + .await; + limit_reached |= has_reached_limit; let mut new_ranges = project_search .update(cx, |project_search, cx| { project_search.excerpts.update(cx, |excerpts, cx| { @@ -352,7 +359,6 @@ impl ProjectSearch { }) }) .ok()?; - while let Some(new_ranges) = new_ranges.next().await { project_search .update(cx, |project_search, cx| { diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 5f8253e2df..a4d3f61141 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -1318,7 +1318,8 @@ impl LocalWorktree { let entry = self.refresh_entry(path.clone(), None, cx); let is_private = self.is_path_private(path.as_ref()); - cx.spawn(async move |this, _cx| { + let this = cx.weak_entity(); + cx.background_spawn(async move { // WARN: Temporary workaround for #27283. // We are not efficient with our memory usage per file, and use in excess of 64GB for a 10GB file // Therefore, as a temporary workaround to prevent system freezes, we just bail before opening a file @@ -1702,6 +1703,7 @@ impl LocalWorktree { }; let t0 = Instant::now(); let mut refresh = self.refresh_entries_for_paths(paths); + // todo(lw): Hot foreground spawn cx.spawn(async move |this, cx| { refresh.recv().await; log::trace!("refreshed entry {path:?} in {:?}", t0.elapsed()); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index d712f782ca..2d7d47e968 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -2860,16 +2860,20 @@ mod tests { }); // Split the pane with the first entry, then open the second entry again. - let (task1, task2) = window + window .update(cx, |w, window, cx| { - ( - w.split_and_clone(w.active_pane().clone(), SplitDirection::Right, window, cx), - w.open_path(file2.clone(), None, true, window, cx), - ) + w.split_and_clone(w.active_pane().clone(), SplitDirection::Right, window, cx) }) + .unwrap() + .await + .unwrap(); + window + .update(cx, |w, window, cx| { + w.open_path(file2.clone(), None, true, window, cx) + }) + .unwrap() + .await .unwrap(); - task1.await.unwrap(); - task2.await.unwrap(); window .read_with(cx, |w, cx| {