perf(daemon): answer search polls with counts instead of copies

A CLI one-off search leaves its result set in the daemon until someone
forgets it, and an attached TUI made that permanent state expensive:
every 100ms tick asked for the search list twice, and each answer
deep-cloned every result of every search under the context lock just to
sum file counts - stalling the thread that stores incoming results.
Worse, any active or selected search pulled its entire result set over
the socket again each tick, megabytes of JSON repeating what the window
already held.

search.list is now backed by search_file_counts, a walk of the cache
instead of a copy of it; the TUI syncs once per tick and re-fetches a
result set only when the session's file count differs from what it
holds. get_search_results_count reads the same summary, so get --pick
first against a daemon polls a count instead of the full set, and both
session kinds now count files where the local one counted peer
responses.
This commit is contained in:
Michel de Graaf
2026-08-09 22:16:27 +02:00
parent d11326f149
commit 1551fbbf1b
6 changed files with 283 additions and 37 deletions
+15 -4
View File
@@ -86,6 +86,8 @@ pub trait SessionApi: Send + Sync {
/// Drop a search from the session, so every window loses it rather than
/// only the one that dismissed it.
fn forget_search(&self, query: &str) -> bool;
/// How many files a search has collected so far — a count cheap enough
/// to poll, never a copy of the results.
fn get_search_results_count(&self, key: &str) -> usize;
fn try_get_search_results(&self, key: &str) -> Option<Vec<SearchResult>>;
fn start_wishlist_search(&self, query: &str) -> Result<()>;
@@ -198,11 +200,15 @@ impl SessionApi for Client {
}
fn all_searches(&self) -> Vec<SessionSearch> {
Self::get_all_searches(self)
// Counts, not copies: the daemon answers this for every attached
// client many times a second, and cloning every result set to sum
// its lengths held the context lock against the thread storing new
// results.
Self::search_file_counts(self)
.into_iter()
.map(|(query, search)| SessionSearch {
.map(|(query, files)| SessionSearch {
query,
files: search.results.iter().map(|r| r.files.len()).sum(),
files,
// A local window started every search it can see, so it
// already knows which are still running.
started_secs_ago: None,
@@ -211,7 +217,12 @@ impl SessionApi for Client {
}
fn get_search_results_count(&self, key: &str) -> usize {
Self::get_search_results_count(self, key)
// Files, agreeing with the remote session — the library's own
// accessor of this name counts per-peer responses instead.
Self::search_file_counts(self)
.into_iter()
.find(|(query, _)| query == key)
.map_or(0, |(_, files)| files)
}
fn try_get_search_results(&self, key: &str) -> Option<Vec<SearchResult>> {