From 3de382cd73f73c7041f4b60c5fa720c5f72d7174 Mon Sep 17 00:00:00 2001 From: vikingowl Date: Sun, 29 Mar 2026 20:33:29 +0200 Subject: [PATCH] perf(search): score by reference, clone only top-N results MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactor search_with_frecency to score static provider items by reference (&LaunchItem, i64) instead of cloning every match. Use select_nth_unstable_by for O(n) partial sort, then clone only the max_results survivors. Reduces clones from O(total_matches) to O(max_results) — typically from hundreds to ~15. --- crates/owlry-core/src/providers/mod.rs | 61 ++++++++++++++++---------- 1 file changed, 38 insertions(+), 23 deletions(-) diff --git a/crates/owlry-core/src/providers/mod.rs b/crates/owlry-core/src/providers/mod.rs index a9c8edc..5b2f7ff 100644 --- a/crates/owlry-core/src/providers/mod.rs +++ b/crates/owlry-core/src/providers/mod.rs @@ -737,22 +737,17 @@ impl ProviderManager { // Empty query (after checking special providers) - return frecency-sorted items if query.is_empty() { - // Collect items from core providers - let core_items = self + let mut scored_refs: Vec<(&LaunchItem, i64)> = self .providers .iter() .filter(|p| filter.is_active(p.provider_type())) - .flat_map(|p| p.items().iter().cloned()); - - // Collect items from static native providers - let native_items = self - .static_native_providers - .iter() - .filter(|p| filter.is_active(p.provider_type())) - .flat_map(|p| p.items().iter().cloned()); - - let items: Vec<(LaunchItem, i64)> = core_items - .chain(native_items) + .flat_map(|p| p.items().iter()) + .chain( + self.static_native_providers + .iter() + .filter(|p| filter.is_active(p.provider_type())) + .flat_map(|p| p.items().iter()), + ) .filter(|item| { // Apply tag filter if present if let Some(tag) = tag_filter { @@ -768,8 +763,15 @@ impl ProviderManager { }) .collect(); - // Combine widgets (already in results) with frecency items - results.extend(items); + // Partial sort: O(n) average to find top max_results, then O(k log k) to order them + if scored_refs.len() > max_results { + scored_refs.select_nth_unstable_by(max_results, |a, b| b.1.cmp(&a.1)); + scored_refs.truncate(max_results); + } + scored_refs.sort_by(|a, b| b.1.cmp(&a.1)); + + // Clone only the survivors + results.extend(scored_refs.into_iter().map(|(item, score)| (item.clone(), score))); results.sort_by(|a, b| b.1.cmp(&a.1)); results.truncate(max_results); return results; @@ -777,7 +779,7 @@ impl ProviderManager { // Regular search with frecency boost and tag matching // Helper closure for scoring items - let score_item = |item: &LaunchItem| -> Option<(LaunchItem, i64)> { + let score_item = |item: &LaunchItem| -> Option { // Apply tag filter if present if let Some(tag) = tag_filter && !item.tags.iter().any(|t| t.to_lowercase().contains(tag)) @@ -824,33 +826,46 @@ impl ProviderManager { 0 }; - (item.clone(), s + frecency_boost + exact_match_boost) + s + frecency_boost + exact_match_boost }) }; - // Search core providers + // Score static items by reference (no cloning) + let mut scored_refs: Vec<(&LaunchItem, i64)> = Vec::new(); + for provider in &self.providers { if !filter.is_active(provider.provider_type()) { continue; } for item in provider.items() { - if let Some(scored) = score_item(item) { - results.push(scored); + if let Some(score) = score_item(item) { + scored_refs.push((item, score)); } } } - // Search static native providers for provider in &self.static_native_providers { if !filter.is_active(provider.provider_type()) { continue; } for item in provider.items() { - if let Some(scored) = score_item(item) { - results.push(scored); + if let Some(score) = score_item(item) { + scored_refs.push((item, score)); } } } + + // Partial sort: O(n) average to find top max_results, then O(k log k) to order them + if scored_refs.len() > max_results { + scored_refs.select_nth_unstable_by(max_results, |a, b| b.1.cmp(&a.1)); + scored_refs.truncate(max_results); + } + scored_refs.sort_by(|a, b| b.1.cmp(&a.1)); + + // Clone only the survivors + results.extend(scored_refs.into_iter().map(|(item, score)| (item.clone(), score))); + + // Final sort merges dynamic results (already in `results`) with static top-N results.sort_by(|a, b| b.1.cmp(&a.1)); results.truncate(max_results);