Skip to content

Commit

Permalink
Improve project search performance (#20211)
Browse files Browse the repository at this point in the history
Follow-up of #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:

* #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
  • Loading branch information
SomeoneToIgnore authored Nov 5, 2024
1 parent 81dd4ca commit 3856599
Show file tree
Hide file tree
Showing 4 changed files with 331 additions and 245 deletions.
129 changes: 74 additions & 55 deletions crates/editor/src/linked_editing_ranges.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::ops::Range;
use std::{ops::Range, time::Duration};

use collections::HashMap;
use itertools::Itertools;
Expand Down Expand Up @@ -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;
}
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,
));
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 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![];
Expand Down Expand Up @@ -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));
}

cx.notify();
})
.log_err();
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();
})
.ok()?;

Some(())
}));
Expand Down
91 changes: 59 additions & 32 deletions crates/go_to_line/src/cursor_position.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use editor::{Editor, ToPoint};
use gpui::{AppContext, Subscription, View, WeakView};
use gpui::{AppContext, Subscription, Task, View, WeakView};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
use std::fmt::Write;
use std::{fmt::Write, time::Duration};
use text::{Point, Selection};
use ui::{
div, Button, ButtonCommon, Clickable, FluentBuilder, IntoElement, LabelSize, ParentElement,
Expand All @@ -23,6 +23,7 @@ pub struct CursorPosition {
position: Option<Point>,
selected_count: SelectionStats,
workspace: WeakView<Workspace>,
update_position: Task<()>,
_observe_active_editor: Option<Subscription>,
}

Expand All @@ -32,40 +33,61 @@ impl CursorPosition {
position: None,
selected_count: Default::default(),
workspace: workspace.weak_handle(),
update_position: Task::ready(()),
_observe_active_editor: None,
}
}

fn update_position(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
editor.update(cx, |editor, cx| {
let buffer = editor.buffer().read(cx).snapshot(cx);

self.selected_count = Default::default();
self.selected_count.selections = editor.selections.count();
let mut last_selection: Option<Selection<usize>> = None;
for selection in editor.selections.all::<usize>(cx) {
self.selected_count.characters += buffer
.text_for_range(selection.start..selection.end)
.map(|t| t.chars().count())
.sum::<usize>();
if last_selection
.as_ref()
.map_or(true, |last_selection| selection.id > last_selection.id)
{
last_selection = Some(selection);
}
}
for selection in editor.selections.all::<Point>(cx) {
if selection.end != selection.start {
self.selected_count.lines += (selection.end.row - selection.start.row) as usize;
if selection.end.column != 0 {
self.selected_count.lines += 1;
}
}
fn update_position(
&mut self,
editor: View<Editor>,
debounce: Option<Duration>,
cx: &mut ViewContext<Self>,
) {
let editor = editor.downgrade();
self.update_position = cx.spawn(|cursor_position, mut cx| async move {
if let Some(debounce) = debounce {
cx.background_executor().timer(debounce).await;
}
self.position = last_selection.map(|s| s.head().to_point(&buffer));

editor
.update(&mut cx, |editor, cx| {
let buffer = editor.buffer().read(cx).snapshot(cx);
cursor_position.update(cx, |cursor_position, cx| {
cursor_position.selected_count = SelectionStats::default();
cursor_position.selected_count.selections = editor.selections.count();
let mut last_selection = None::<Selection<usize>>;
for selection in editor.selections.all::<usize>(cx) {
cursor_position.selected_count.characters += buffer
.text_for_range(selection.start..selection.end)
.map(|t| t.chars().count())
.sum::<usize>();
if last_selection
.as_ref()
.map_or(true, |last_selection| selection.id > last_selection.id)
{
last_selection = Some(selection);
}
}
for selection in editor.selections.all::<Point>(cx) {
if selection.end != selection.start {
cursor_position.selected_count.lines +=
(selection.end.row - selection.start.row) as usize;
if selection.end.column != 0 {
cursor_position.selected_count.lines += 1;
}
}
}
cursor_position.position =
last_selection.map(|s| s.head().to_point(&buffer));
cx.notify();
})
})
.ok()
.transpose()
.ok()
.flatten();
});
cx.notify();
}

fn write_position(&self, text: &mut String, cx: &AppContext) {
Expand Down Expand Up @@ -154,15 +176,20 @@ impl Render for CursorPosition {
}
}

const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);

impl StatusItemView for CursorPosition {
fn set_active_pane_item(
&mut self,
active_pane_item: Option<&dyn ItemHandle>,
cx: &mut ViewContext<Self>,
) {
if let Some(editor) = active_pane_item.and_then(|item| item.act_as::<Editor>(cx)) {
self._observe_active_editor = Some(cx.observe(&editor, Self::update_position));
self.update_position(editor, cx);
self._observe_active_editor =
Some(cx.observe(&editor, |cursor_position, editor, cx| {
Self::update_position(cursor_position, editor, Some(UPDATE_DEBOUNCE), cx)
}));
self.update_position(editor, None, cx);
} else {
self.position = None;
self._observe_active_editor = None;
Expand Down
4 changes: 3 additions & 1 deletion crates/go_to_line/src/go_to_line.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ mod tests {
use indoc::indoc;
use project::{FakeFs, Project};
use serde_json::json;
use std::sync::Arc;
use std::{sync::Arc, time::Duration};
use workspace::{AppState, Workspace};

#[gpui::test]
Expand Down Expand Up @@ -379,6 +379,7 @@ mod tests {
.downcast::<Editor>()
.unwrap();

cx.executor().advance_clock(Duration::from_millis(200));
workspace.update(cx, |workspace, cx| {
assert_eq!(
&SelectionStats {
Expand All @@ -397,6 +398,7 @@ mod tests {
);
});
editor.update(cx, |editor, cx| editor.select_all(&SelectAll, cx));
cx.executor().advance_clock(Duration::from_millis(200));
workspace.update(cx, |workspace, cx| {
assert_eq!(
&SelectionStats {
Expand Down
Loading

0 comments on commit 3856599

Please sign in to comment.