Improve project search performance (#20211)
Follow-up of https://github.com/zed-industries/zed/pull/20171 Reduces time Zed needs to reach maximum search results by an order of a magnitude. Methodology: * plugged-in mac with Instruments and Zed open * Zed is restarted before each measurement, `zed` project is opened, a *.rs file is opened and rust-analyzer is fully loaded, file is closed then * from an "empty" state, a `test` word is searched in the project search * each version is checked with project panel; and then, separately, without it * after we reach maximum test results (the counter stops at `10191+`), the measurement stops Zed Dev is compiled and installed with `./script/bundle-mac -li` ------------------------ [measurements.trace.zip](https://github.com/user-attachments/files/17625516/measurements.trace.zip) Before: * Zed Nightly with outline panel open <img width="1113" alt="image" src="https://github.com/user-attachments/assets/62b29a69-c266-4d46-8c3c-0e9534ca7967"> Took over 30s to load the result set * Zed Nightly without outline panel <img width="1109" alt="image" src="https://github.com/user-attachments/assets/82d8d9d6-e8f2-4e67-af55-3f54a7c1d92d"> Took over 24s to load the result set * Zed Dev with outline panel open <img width="1131" alt="image" src="https://github.com/user-attachments/assets/15605ff8-0787-428e-bbb6-f8496f7e1d43"> Took around 6s to load the result set (the profile was running a bit longer) * Zed Dev without outline panel <img width="1147" alt="image" src="https://github.com/user-attachments/assets/0715d73e-f41a-4d74-a604-a3a96ad8d585"> Took around 5s to load the result set --------------------- Improvements in the outline panel: * https://github.com/zed-industries/zed/pull/20171 ensured we reuse previous rendered search results from the outline panel * all search results are now rendered in the background thread * only the entries that are rendered with gpui are sent to the background thread for rendering * FS entries' update logic does nothing before the debounce now Improvements in the editor: * cursor update operations are debounced and all calculations start after the debounce only * linked edits are now debounced and all work is done after the debounce only Further possible improvements: * we could batch calculations of text coordinates, related to the search entries: right now, each search match range is expanded around and clipped, then fitted to the closest surrounding whitespace (if any, otherwise it's just trimmed). Each such calculation requires multiple tree traversals, which is suboptimal and causes more CPU usage than we could use. * linked edits are always calculated, even if the language settings have it disabled, or the corresponding language having no corresponding capabilities Release Notes: - Improve large project search performance
This commit is contained in:
parent
81dd4ca1c9
commit
3856599853
4 changed files with 330 additions and 244 deletions
|
@ -1,4 +1,4 @@
|
|||
use std::ops::Range;
|
||||
use std::{ops::Range, time::Duration};
|
||||
|
||||
use collections::HashMap;
|
||||
use itertools::Itertools;
|
||||
|
@ -36,35 +36,53 @@ impl LinkedEditingRanges {
|
|||
self.0.is_empty()
|
||||
}
|
||||
}
|
||||
pub(super) fn refresh_linked_ranges(this: &mut Editor, cx: &mut ViewContext<Editor>) -> Option<()> {
|
||||
if this.pending_rename.is_some() {
|
||||
|
||||
const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);
|
||||
|
||||
// TODO do not refresh anything at all, if the settings/capabilities do not have it enabled.
|
||||
pub(super) fn refresh_linked_ranges(
|
||||
editor: &mut Editor,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> Option<()> {
|
||||
if editor.pending_rename.is_some() {
|
||||
return None;
|
||||
}
|
||||
let project = this.project.clone()?;
|
||||
let selections = this.selections.all::<usize>(cx);
|
||||
let buffer = this.buffer.read(cx);
|
||||
let mut applicable_selections = vec![];
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
for selection in selections {
|
||||
let cursor_position = selection.head();
|
||||
let start_position = snapshot.anchor_before(cursor_position);
|
||||
let end_position = snapshot.anchor_after(selection.tail());
|
||||
if start_position.buffer_id != end_position.buffer_id || end_position.buffer_id.is_none() {
|
||||
// Throw away selections spanning multiple buffers.
|
||||
continue;
|
||||
let project = editor.project.as_ref()?.downgrade();
|
||||
|
||||
editor.linked_editing_range_task = Some(cx.spawn(|editor, mut cx| async move {
|
||||
cx.background_executor().timer(UPDATE_DEBOUNCE).await;
|
||||
|
||||
let mut applicable_selections = Vec::new();
|
||||
editor
|
||||
.update(&mut cx, |editor, cx| {
|
||||
let selections = editor.selections.all::<usize>(cx);
|
||||
let snapshot = editor.buffer.read(cx).snapshot(cx);
|
||||
let buffer = editor.buffer.read(cx);
|
||||
for selection in selections {
|
||||
let cursor_position = selection.head();
|
||||
let start_position = snapshot.anchor_before(cursor_position);
|
||||
let end_position = snapshot.anchor_after(selection.tail());
|
||||
if start_position.buffer_id != end_position.buffer_id
|
||||
|| end_position.buffer_id.is_none()
|
||||
{
|
||||
// Throw away selections spanning multiple buffers.
|
||||
continue;
|
||||
}
|
||||
if let Some(buffer) = end_position.buffer_id.and_then(|id| buffer.buffer(id)) {
|
||||
applicable_selections.push((
|
||||
buffer,
|
||||
start_position.text_anchor,
|
||||
end_position.text_anchor,
|
||||
));
|
||||
}
|
||||
}
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
if applicable_selections.is_empty() {
|
||||
return None;
|
||||
}
|
||||
if let Some(buffer) = end_position.buffer_id.and_then(|id| buffer.buffer(id)) {
|
||||
applicable_selections.push((
|
||||
buffer,
|
||||
start_position.text_anchor,
|
||||
end_position.text_anchor,
|
||||
));
|
||||
}
|
||||
}
|
||||
if applicable_selections.is_empty() {
|
||||
return None;
|
||||
}
|
||||
this.linked_editing_range_task = Some(cx.spawn(|this, mut cx| async move {
|
||||
|
||||
let highlights = project
|
||||
.update(&mut cx, |project, cx| {
|
||||
let mut linked_edits_tasks = vec![];
|
||||
|
@ -110,37 +128,38 @@ pub(super) fn refresh_linked_ranges(this: &mut Editor, cx: &mut ViewContext<Edit
|
|||
}
|
||||
linked_edits_tasks
|
||||
})
|
||||
.log_err()?;
|
||||
.ok()?;
|
||||
|
||||
let highlights = futures::future::join_all(highlights).await;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.linked_edit_ranges.0.clear();
|
||||
if this.pending_rename.is_some() {
|
||||
return;
|
||||
}
|
||||
for (buffer_id, ranges) in highlights.into_iter().flatten() {
|
||||
this.linked_edit_ranges
|
||||
.0
|
||||
.entry(buffer_id)
|
||||
.or_default()
|
||||
.extend(ranges);
|
||||
}
|
||||
for (buffer_id, values) in this.linked_edit_ranges.0.iter_mut() {
|
||||
let Some(snapshot) = this
|
||||
.buffer
|
||||
.read(cx)
|
||||
.buffer(*buffer_id)
|
||||
.map(|buffer| buffer.read(cx).snapshot())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
values.sort_by(|lhs, rhs| lhs.0.cmp(&rhs.0, &snapshot));
|
||||
}
|
||||
editor
|
||||
.update(&mut cx, |this, cx| {
|
||||
this.linked_edit_ranges.0.clear();
|
||||
if this.pending_rename.is_some() {
|
||||
return;
|
||||
}
|
||||
for (buffer_id, ranges) in highlights.into_iter().flatten() {
|
||||
this.linked_edit_ranges
|
||||
.0
|
||||
.entry(buffer_id)
|
||||
.or_default()
|
||||
.extend(ranges);
|
||||
}
|
||||
for (buffer_id, values) in this.linked_edit_ranges.0.iter_mut() {
|
||||
let Some(snapshot) = this
|
||||
.buffer
|
||||
.read(cx)
|
||||
.buffer(*buffer_id)
|
||||
.map(|buffer| buffer.read(cx).snapshot())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
values.sort_by(|lhs, rhs| lhs.0.cmp(&rhs.0, &snapshot));
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
})
|
||||
.log_err();
|
||||
cx.notify();
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
Some(())
|
||||
}));
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue