diagnostics: Improve performance with large # of diagnostics (#20189)

Related to: https://github.com/zed-industries/zed/issues/19022

Release Notes:

- Improve editor performance with large # of diagnostics.

---------

Co-authored-by: Conrad <conrad@zed.dev>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
This commit is contained in:
Piotr Osiewicz 2024-11-04 20:16:02 +01:00 committed by GitHub
parent 77de20c23a
commit dc5fad52a3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 65 additions and 70 deletions

1
Cargo.lock generated
View file

@ -3491,7 +3491,6 @@ dependencies = [
"ctor", "ctor",
"editor", "editor",
"env_logger 0.11.5", "env_logger 0.11.5",
"futures 0.3.30",
"gpui", "gpui",
"language", "language",
"log", "log",

View file

@ -18,7 +18,6 @@ collections.workspace = true
ctor.workspace = true ctor.workspace = true
editor.workspace = true editor.workspace = true
env_logger.workspace = true env_logger.workspace = true
futures.workspace = true
gpui.workspace = true gpui.workspace = true
language.workspace = true language.workspace = true
log.workspace = true log.workspace = true

View file

@ -14,10 +14,6 @@ use editor::{
scroll::Autoscroll, scroll::Autoscroll,
Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer, ToOffset, Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer, ToOffset,
}; };
use futures::{
channel::mpsc::{self, UnboundedSender},
StreamExt as _,
};
use gpui::{ use gpui::{
actions, div, svg, AnyElement, AnyView, AppContext, Context, EventEmitter, FocusHandle, actions, div, svg, AnyElement, AnyView, AppContext, Context, EventEmitter, FocusHandle,
FocusableView, HighlightStyle, InteractiveElement, IntoElement, Model, ParentElement, Render, FocusableView, HighlightStyle, InteractiveElement, IntoElement, Model, ParentElement, Render,
@ -62,11 +58,10 @@ struct ProjectDiagnosticsEditor {
summary: DiagnosticSummary, summary: DiagnosticSummary,
excerpts: Model<MultiBuffer>, excerpts: Model<MultiBuffer>,
path_states: Vec<PathState>, path_states: Vec<PathState>,
paths_to_update: BTreeSet<(ProjectPath, LanguageServerId)>, paths_to_update: BTreeSet<(ProjectPath, Option<LanguageServerId>)>,
include_warnings: bool, include_warnings: bool,
context: u32, context: u32,
update_paths_tx: UnboundedSender<(ProjectPath, Option<LanguageServerId>)>, update_excerpts_task: Option<Task<Result<()>>>,
_update_excerpts_task: Task<Result<()>>,
_subscription: Subscription, _subscription: Subscription,
} }
@ -129,14 +124,14 @@ impl ProjectDiagnosticsEditor {
} }
project::Event::DiskBasedDiagnosticsFinished { language_server_id } => { project::Event::DiskBasedDiagnosticsFinished { language_server_id } => {
log::debug!("disk based diagnostics finished for server {language_server_id}"); log::debug!("disk based diagnostics finished for server {language_server_id}");
this.enqueue_update_stale_excerpts(Some(*language_server_id)); this.update_stale_excerpts(cx);
} }
project::Event::DiagnosticsUpdated { project::Event::DiagnosticsUpdated {
language_server_id, language_server_id,
path, path,
} => { } => {
this.paths_to_update this.paths_to_update
.insert((path.clone(), *language_server_id)); .insert((path.clone(), Some(*language_server_id)));
this.summary = project.read(cx).diagnostic_summary(false, cx); this.summary = project.read(cx).diagnostic_summary(false, cx);
cx.emit(EditorEvent::TitleChanged); cx.emit(EditorEvent::TitleChanged);
@ -144,7 +139,7 @@ impl ProjectDiagnosticsEditor {
log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. recording change"); log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. recording change");
} else { } else {
log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. updating excerpts"); log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. updating excerpts");
this.enqueue_update_stale_excerpts(Some(*language_server_id)); this.update_stale_excerpts(cx);
} }
} }
_ => {} _ => {}
@ -171,14 +166,12 @@ impl ProjectDiagnosticsEditor {
cx.focus(&this.focus_handle); cx.focus(&this.focus_handle);
} }
} }
EditorEvent::Blurred => this.enqueue_update_stale_excerpts(None), EditorEvent::Blurred => this.update_stale_excerpts(cx),
_ => {} _ => {}
} }
}) })
.detach(); .detach();
let (update_excerpts_tx, mut update_excerpts_rx) = mpsc::unbounded();
let project = project_handle.read(cx); let project = project_handle.read(cx);
let mut this = Self { let mut this = Self {
project: project_handle.clone(), project: project_handle.clone(),
@ -191,27 +184,45 @@ impl ProjectDiagnosticsEditor {
path_states: Default::default(), path_states: Default::default(),
paths_to_update: Default::default(), paths_to_update: Default::default(),
include_warnings: ProjectDiagnosticsSettings::get_global(cx).include_warnings, include_warnings: ProjectDiagnosticsSettings::get_global(cx).include_warnings,
update_paths_tx: update_excerpts_tx, update_excerpts_task: None,
_update_excerpts_task: cx.spawn(move |this, mut cx| async move {
while let Some((path, language_server_id)) = update_excerpts_rx.next().await {
if let Some(buffer) = project_handle
.update(&mut cx, |project, cx| project.open_buffer(path.clone(), cx))?
.await
.log_err()
{
this.update(&mut cx, |this, cx| {
this.update_excerpts(path, language_server_id, buffer, cx);
})?;
}
}
anyhow::Ok(())
}),
_subscription: project_event_subscription, _subscription: project_event_subscription,
}; };
this.enqueue_update_all_excerpts(cx); this.update_all_excerpts(cx);
this this
} }
fn update_stale_excerpts(&mut self, cx: &mut ViewContext<Self>) {
if self.update_excerpts_task.is_some() {
return;
}
let project_handle = self.project.clone();
self.update_excerpts_task = Some(cx.spawn(|this, mut cx| async move {
loop {
let Some((path, language_server_id)) = this.update(&mut cx, |this, _| {
let Some((path, language_server_id)) = this.paths_to_update.pop_first() else {
this.update_excerpts_task.take();
return None;
};
Some((path, language_server_id))
})?
else {
break;
};
if let Some(buffer) = project_handle
.update(&mut cx, |project, cx| project.open_buffer(path.clone(), cx))?
.await
.log_err()
{
this.update(&mut cx, |this, cx| {
this.update_excerpts(path, language_server_id, buffer, cx);
})?;
}
}
Ok(())
}));
}
fn new( fn new(
project_handle: Model<Project>, project_handle: Model<Project>,
workspace: WeakView<Workspace>, workspace: WeakView<Workspace>,
@ -239,7 +250,7 @@ impl ProjectDiagnosticsEditor {
fn toggle_warnings(&mut self, _: &ToggleWarnings, cx: &mut ViewContext<Self>) { fn toggle_warnings(&mut self, _: &ToggleWarnings, cx: &mut ViewContext<Self>) {
self.include_warnings = !self.include_warnings; self.include_warnings = !self.include_warnings;
self.enqueue_update_all_excerpts(cx); self.update_all_excerpts(cx);
cx.notify(); cx.notify();
} }
@ -251,37 +262,28 @@ impl ProjectDiagnosticsEditor {
fn focus_out(&mut self, cx: &mut ViewContext<Self>) { fn focus_out(&mut self, cx: &mut ViewContext<Self>) {
if !self.focus_handle.is_focused(cx) && !self.editor.focus_handle(cx).is_focused(cx) { if !self.focus_handle.is_focused(cx) && !self.editor.focus_handle(cx).is_focused(cx) {
self.enqueue_update_stale_excerpts(None); self.update_stale_excerpts(cx);
} }
} }
/// Enqueue an update of all excerpts. Updates all paths that either /// Enqueue an update of all excerpts. Updates all paths that either
/// currently have diagnostics or are currently present in this view. /// currently have diagnostics or are currently present in this view.
fn enqueue_update_all_excerpts(&mut self, cx: &mut ViewContext<Self>) { fn update_all_excerpts(&mut self, cx: &mut ViewContext<Self>) {
self.project.update(cx, |project, cx| { self.project.update(cx, |project, cx| {
let mut paths = project let mut paths = project
.diagnostic_summaries(false, cx) .diagnostic_summaries(false, cx)
.map(|(path, _, _)| path) .map(|(path, _, _)| (path, None))
.collect::<BTreeSet<_>>(); .collect::<BTreeSet<_>>();
paths.extend(self.path_states.iter().map(|state| state.path.clone())); paths.extend(
for path in paths { self.path_states
self.update_paths_tx.unbounded_send((path, None)).unwrap(); .iter()
} .map(|state| (state.path.clone(), None)),
);
let paths_to_update = std::mem::take(&mut self.paths_to_update);
paths.extend(paths_to_update.into_iter().map(|(path, _)| (path, None)));
self.paths_to_update = paths;
}); });
} self.update_stale_excerpts(cx);
/// Enqueue an update of the excerpts for any path whose diagnostics are known
/// to have changed. If a language server id is passed, then only the excerpts for
/// that language server's diagnostics will be updated. Otherwise, all stale excerpts
/// will be refreshed.
fn enqueue_update_stale_excerpts(&mut self, language_server_id: Option<LanguageServerId>) {
for (path, server_id) in &self.paths_to_update {
if language_server_id.map_or(true, |id| id == *server_id) {
self.update_paths_tx
.unbounded_send((path.clone(), Some(*server_id)))
.unwrap();
}
}
} }
fn update_excerpts( fn update_excerpts(
@ -291,11 +293,6 @@ impl ProjectDiagnosticsEditor {
buffer: Model<Buffer>, buffer: Model<Buffer>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
self.paths_to_update.retain(|(path, server_id)| {
*path != path_to_update
|| server_to_update.map_or(false, |to_update| *server_id != to_update)
});
let was_empty = self.path_states.is_empty(); let was_empty = self.path_states.is_empty();
let snapshot = buffer.read(cx).snapshot(); let snapshot = buffer.read(cx).snapshot();
let path_ix = match self let path_ix = match self

View file

@ -800,7 +800,7 @@ async fn test_random_diagnostics(cx: &mut TestAppContext, mut rng: StdRng) {
} }
log::info!("updating mutated diagnostics view"); log::info!("updating mutated diagnostics view");
mutated_view.update(cx, |view, _| view.enqueue_update_stale_excerpts(None)); mutated_view.update(cx, |view, cx| view.update_stale_excerpts(cx));
cx.run_until_parked(); cx.run_until_parked();
log::info!("constructing reference diagnostics view"); log::info!("constructing reference diagnostics view");

View file

@ -14,12 +14,12 @@ impl Render for ToolbarControls {
let mut has_stale_excerpts = false; let mut has_stale_excerpts = false;
let mut is_updating = false; let mut is_updating = false;
if let Some(editor) = self.editor() { if let Some(editor) = self.diagnostics() {
let editor = editor.read(cx); let diagnostics = editor.read(cx);
include_warnings = editor.include_warnings; include_warnings = diagnostics.include_warnings;
has_stale_excerpts = !editor.paths_to_update.is_empty(); has_stale_excerpts = !diagnostics.paths_to_update.is_empty();
is_updating = !editor.update_paths_tx.is_empty() is_updating = diagnostics.update_excerpts_task.is_some()
|| editor || diagnostics
.project .project
.read(cx) .read(cx)
.language_servers_running_disk_based_diagnostics(cx) .language_servers_running_disk_based_diagnostics(cx)
@ -49,9 +49,9 @@ impl Render for ToolbarControls {
.disabled(is_updating) .disabled(is_updating)
.tooltip(move |cx| Tooltip::text("Update excerpts", cx)) .tooltip(move |cx| Tooltip::text("Update excerpts", cx))
.on_click(cx.listener(|this, _, cx| { .on_click(cx.listener(|this, _, cx| {
if let Some(editor) = this.editor() { if let Some(diagnostics) = this.diagnostics() {
editor.update(cx, |editor, _| { diagnostics.update(cx, |diagnostics, cx| {
editor.enqueue_update_stale_excerpts(None); diagnostics.update_all_excerpts(cx);
}); });
} }
})), })),
@ -63,7 +63,7 @@ impl Render for ToolbarControls {
.shape(IconButtonShape::Square) .shape(IconButtonShape::Square)
.tooltip(move |cx| Tooltip::text(tooltip, cx)) .tooltip(move |cx| Tooltip::text(tooltip, cx))
.on_click(cx.listener(|this, _, cx| { .on_click(cx.listener(|this, _, cx| {
if let Some(editor) = this.editor() { if let Some(editor) = this.diagnostics() {
editor.update(cx, |editor, cx| { editor.update(cx, |editor, cx| {
editor.toggle_warnings(&Default::default(), cx); editor.toggle_warnings(&Default::default(), cx);
}); });
@ -105,7 +105,7 @@ impl ToolbarControls {
ToolbarControls { editor: None } ToolbarControls { editor: None }
} }
fn editor(&self) -> Option<View<ProjectDiagnosticsEditor>> { fn diagnostics(&self) -> Option<View<ProjectDiagnosticsEditor>> {
self.editor.as_ref()?.upgrade() self.editor.as_ref()?.upgrade()
} }
} }