search: New old search implementation (#39956)
This is an in-progress work on changing how task scheduler affects performance of project search. Instead of relying on tasks being executed at a discretion of the task scheduler, we want to experiment with having a set of "agents" that prioritize driving in-progress project search matches to completion over pushing the whole thing to completion. This should hopefully significantly improve throughput & latency of project search. Release Notes: - Improved project search performance --------- Co-authored-by: Smit Barmase <smit@zed.dev> Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
This commit is contained in:
co-authored by
Smit Barmase
Smit Barmase
parent
85c2aa7325
commit
7c4fb5a899
+57
-178
@@ -11,6 +11,7 @@ pub mod lsp_command;
|
||||
pub mod lsp_store;
|
||||
mod manifest_tree;
|
||||
pub mod prettier_store;
|
||||
mod project_search;
|
||||
pub mod project_settings;
|
||||
pub mod search;
|
||||
mod task_inventory;
|
||||
@@ -39,6 +40,7 @@ use crate::{
|
||||
agent_server_store::AllAgentServersSettings,
|
||||
git_store::GitStore,
|
||||
lsp_store::{SymbolLocation, log_store::LogKind},
|
||||
project_search::SearchResultsHandle,
|
||||
};
|
||||
pub use agent_server_store::{AgentServerStore, AgentServersUpdated};
|
||||
pub use git_store::{
|
||||
@@ -46,6 +48,7 @@ pub use git_store::{
|
||||
git_traversal::{ChildEntriesGitIter, GitEntry, GitEntryRef, GitTraversal},
|
||||
};
|
||||
pub use manifest_tree::ManifestTree;
|
||||
pub use project_search::Search;
|
||||
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use buffer_store::{BufferStore, BufferStoreEvent};
|
||||
@@ -109,7 +112,7 @@ use snippet_provider::SnippetProvider;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
collections::BTreeMap,
|
||||
ops::Range,
|
||||
ops::{Not as _, Range},
|
||||
path::{Path, PathBuf},
|
||||
pin::pin,
|
||||
str,
|
||||
@@ -123,7 +126,7 @@ use text::{Anchor, BufferId, OffsetRangeExt, Point, Rope};
|
||||
use toolchain_store::EmptyToolchainStore;
|
||||
use util::{
|
||||
ResultExt as _, maybe,
|
||||
paths::{PathStyle, SanitizedPath, compare_paths, is_absolute},
|
||||
paths::{PathStyle, SanitizedPath, is_absolute},
|
||||
rel_path::RelPath,
|
||||
};
|
||||
use worktree::{CreatedEntry, Snapshot, Traversal};
|
||||
@@ -150,8 +153,6 @@ pub use lsp_store::{
|
||||
};
|
||||
pub use toolchain_store::{ToolchainStore, Toolchains};
|
||||
const MAX_PROJECT_SEARCH_HISTORY_SIZE: usize = 500;
|
||||
const MAX_SEARCH_RESULT_FILES: usize = 5_000;
|
||||
const MAX_SEARCH_RESULT_RANGES: usize = 10_000;
|
||||
|
||||
pub trait ProjectItem: 'static {
|
||||
fn try_open(
|
||||
@@ -3935,179 +3936,44 @@ impl Project {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn search(&mut self, query: SearchQuery, cx: &mut Context<Self>) -> Receiver<SearchResult> {
|
||||
let (result_tx, result_rx) = smol::channel::unbounded();
|
||||
|
||||
let matching_buffers_rx = if query.is_opened_only() {
|
||||
self.sort_search_candidates(&query, cx)
|
||||
} else {
|
||||
self.find_search_candidate_buffers(&query, MAX_SEARCH_RESULT_FILES + 1, cx)
|
||||
};
|
||||
|
||||
cx.spawn(async move |_, cx| {
|
||||
let mut range_count = 0;
|
||||
let mut buffer_count = 0;
|
||||
let mut limit_reached = false;
|
||||
let query = Arc::new(query);
|
||||
let chunks = matching_buffers_rx.ready_chunks(64);
|
||||
|
||||
// Now that we know what paths match the query, we will load at most
|
||||
// 64 buffers at a time to avoid overwhelming the main thread. For each
|
||||
// opened buffer, we will spawn a background task that retrieves all the
|
||||
// ranges in the buffer matched by the query.
|
||||
let mut chunks = pin!(chunks);
|
||||
'outer: while let Some(matching_buffer_chunk) = chunks.next().await {
|
||||
let mut chunk_results = Vec::with_capacity(matching_buffer_chunk.len());
|
||||
for buffer in matching_buffer_chunk {
|
||||
let query = query.clone();
|
||||
let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot())?;
|
||||
chunk_results.push(cx.background_spawn(async move {
|
||||
let ranges = query
|
||||
.search(&snapshot, None)
|
||||
.await
|
||||
.iter()
|
||||
.map(|range| {
|
||||
snapshot.anchor_before(range.start)
|
||||
..snapshot.anchor_after(range.end)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
anyhow::Ok((buffer, ranges))
|
||||
}));
|
||||
}
|
||||
|
||||
let chunk_results = futures::future::join_all(chunk_results).await;
|
||||
for result in chunk_results {
|
||||
if let Some((buffer, ranges)) = result.log_err() {
|
||||
range_count += ranges.len();
|
||||
buffer_count += 1;
|
||||
result_tx
|
||||
.send(SearchResult::Buffer { buffer, ranges })
|
||||
.await?;
|
||||
if buffer_count > MAX_SEARCH_RESULT_FILES
|
||||
|| range_count > MAX_SEARCH_RESULT_RANGES
|
||||
{
|
||||
limit_reached = true;
|
||||
break 'outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if limit_reached {
|
||||
result_tx.send(SearchResult::LimitReached).await?;
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
|
||||
result_rx
|
||||
}
|
||||
|
||||
fn find_search_candidate_buffers(
|
||||
&mut self,
|
||||
query: &SearchQuery,
|
||||
limit: usize,
|
||||
cx: &mut Context<Project>,
|
||||
) -> Receiver<Entity<Buffer>> {
|
||||
if self.is_local() {
|
||||
let fs = self.fs.clone();
|
||||
self.buffer_store.update(cx, |buffer_store, cx| {
|
||||
buffer_store.find_search_candidates(query, limit, fs, cx)
|
||||
})
|
||||
} else {
|
||||
self.find_search_candidates_remote(query, limit, cx)
|
||||
}
|
||||
}
|
||||
|
||||
fn sort_search_candidates(
|
||||
&mut self,
|
||||
search_query: &SearchQuery,
|
||||
cx: &mut Context<Project>,
|
||||
) -> Receiver<Entity<Buffer>> {
|
||||
let worktree_store = self.worktree_store.read(cx);
|
||||
let mut buffers = search_query
|
||||
.buffers()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter(|buffer| {
|
||||
let b = buffer.read(cx);
|
||||
if let Some(file) = b.file() {
|
||||
if !search_query.match_path(file.path().as_std_path()) {
|
||||
return false;
|
||||
}
|
||||
if let Some(entry) = b
|
||||
.entry_id(cx)
|
||||
.and_then(|entry_id| worktree_store.entry_for_id(entry_id, cx))
|
||||
&& entry.is_ignored
|
||||
&& !search_query.include_ignored()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let (tx, rx) = smol::channel::unbounded();
|
||||
buffers.sort_by(|a, b| match (a.read(cx).file(), b.read(cx).file()) {
|
||||
(None, None) => a.read(cx).remote_id().cmp(&b.read(cx).remote_id()),
|
||||
(None, Some(_)) => std::cmp::Ordering::Less,
|
||||
(Some(_), None) => std::cmp::Ordering::Greater,
|
||||
(Some(a), Some(b)) => compare_paths(
|
||||
(a.path().as_std_path(), true),
|
||||
(b.path().as_std_path(), true),
|
||||
),
|
||||
});
|
||||
for buffer in buffers {
|
||||
tx.send_blocking(buffer.clone()).unwrap()
|
||||
}
|
||||
|
||||
rx
|
||||
}
|
||||
|
||||
fn find_search_candidates_remote(
|
||||
&mut self,
|
||||
query: &SearchQuery,
|
||||
limit: usize,
|
||||
cx: &mut Context<Project>,
|
||||
) -> Receiver<Entity<Buffer>> {
|
||||
let (tx, rx) = smol::channel::unbounded();
|
||||
|
||||
let (client, remote_id): (AnyProtoClient, _) = if let Some(ssh_client) = &self.remote_client
|
||||
{
|
||||
(ssh_client.read(cx).proto_client(), 0)
|
||||
fn search_impl(&mut self, query: SearchQuery, cx: &mut Context<Self>) -> SearchResultsHandle {
|
||||
let client: Option<(AnyProtoClient, _)> = if let Some(ssh_client) = &self.remote_client {
|
||||
Some((ssh_client.read(cx).proto_client(), 0))
|
||||
} else if let Some(remote_id) = self.remote_id() {
|
||||
(self.collab_client.clone().into(), remote_id)
|
||||
self.is_local()
|
||||
.not()
|
||||
.then(|| (self.collab_client.clone().into(), remote_id))
|
||||
} else {
|
||||
return rx;
|
||||
None
|
||||
};
|
||||
|
||||
let request = client.request(proto::FindSearchCandidates {
|
||||
project_id: remote_id,
|
||||
query: Some(query.to_proto()),
|
||||
limit: limit as _,
|
||||
});
|
||||
let guard = self.retain_remotely_created_models(cx);
|
||||
|
||||
cx.spawn(async move |project, cx| {
|
||||
let response = request.await?;
|
||||
for buffer_id in response.buffer_ids {
|
||||
let buffer_id = BufferId::new(buffer_id)?;
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.buffer_store.update(cx, |buffer_store, cx| {
|
||||
buffer_store.wait_for_remote_buffer(buffer_id, cx)
|
||||
})
|
||||
})?
|
||||
.await?;
|
||||
let _ = tx.send(buffer).await;
|
||||
let searcher = if query.is_opened_only() {
|
||||
project_search::Search::open_buffers_only(
|
||||
self.buffer_store.clone(),
|
||||
self.worktree_store.clone(),
|
||||
project_search::Search::MAX_SEARCH_RESULT_FILES + 1,
|
||||
)
|
||||
} else {
|
||||
match client {
|
||||
Some((client, remote_id)) => project_search::Search::remote(
|
||||
self.buffer_store.clone(),
|
||||
self.worktree_store.clone(),
|
||||
project_search::Search::MAX_SEARCH_RESULT_FILES + 1,
|
||||
(client, remote_id, self.remotely_created_models.clone()),
|
||||
),
|
||||
None => project_search::Search::local(
|
||||
self.fs.clone(),
|
||||
self.buffer_store.clone(),
|
||||
self.worktree_store.clone(),
|
||||
project_search::Search::MAX_SEARCH_RESULT_FILES + 1,
|
||||
cx,
|
||||
),
|
||||
}
|
||||
};
|
||||
searcher.into_handle(query, cx)
|
||||
}
|
||||
|
||||
drop(guard);
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
rx
|
||||
pub fn search(&mut self, query: SearchQuery, cx: &mut Context<Self>) -> Receiver<SearchResult> {
|
||||
self.search_impl(query, cx).results(cx)
|
||||
}
|
||||
|
||||
pub fn request_lsp<R: LspCommand>(
|
||||
@@ -4832,18 +4698,31 @@ impl Project {
|
||||
fn retain_remotely_created_models(
|
||||
&mut self,
|
||||
cx: &mut Context<Self>,
|
||||
) -> RemotelyCreatedModelGuard {
|
||||
Self::retain_remotely_created_models_impl(
|
||||
&self.remotely_created_models,
|
||||
&self.buffer_store,
|
||||
&self.worktree_store,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
fn retain_remotely_created_models_impl(
|
||||
models: &Arc<Mutex<RemotelyCreatedModels>>,
|
||||
buffer_store: &Entity<BufferStore>,
|
||||
worktree_store: &Entity<WorktreeStore>,
|
||||
cx: &mut App,
|
||||
) -> RemotelyCreatedModelGuard {
|
||||
{
|
||||
let mut remotely_create_models = self.remotely_created_models.lock();
|
||||
let mut remotely_create_models = models.lock();
|
||||
if remotely_create_models.retain_count == 0 {
|
||||
remotely_create_models.buffers = self.buffer_store.read(cx).buffers().collect();
|
||||
remotely_create_models.worktrees =
|
||||
self.worktree_store.read(cx).worktrees().collect();
|
||||
remotely_create_models.buffers = buffer_store.read(cx).buffers().collect();
|
||||
remotely_create_models.worktrees = worktree_store.read(cx).worktrees().collect();
|
||||
}
|
||||
remotely_create_models.retain_count += 1;
|
||||
}
|
||||
RemotelyCreatedModelGuard {
|
||||
remote_models: Arc::downgrade(&self.remotely_created_models),
|
||||
remote_models: Arc::downgrade(&models),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4913,7 +4792,7 @@ impl Project {
|
||||
let query =
|
||||
SearchQuery::from_proto(message.query.context("missing query field")?, path_style)?;
|
||||
let results = this.update(&mut cx, |this, cx| {
|
||||
this.find_search_candidate_buffers(&query, message.limit as _, cx)
|
||||
this.search_impl(query, cx).matching_buffers(cx)
|
||||
})?;
|
||||
|
||||
let mut response = proto::FindSearchCandidatesResponse {
|
||||
|
||||
Reference in New Issue
Block a user