From 6113217f7b0efd8299f883b251d8e8b0548bf501 Mon Sep 17 00:00:00 2001 From: vikingowl Date: Sat, 28 Mar 2026 08:45:21 +0100 Subject: [PATCH] perf(core): sample Utc::now() once per search instead of per-item get_score() called Utc::now() inside calculate_frecency() for every item in the search loop. Added get_score_at() that accepts a pre-sampled timestamp. Eliminates hundreds of unnecessary clock_gettime syscalls per keystroke. --- crates/owlry-core/src/data/frecency.rs | 51 +++++++++++++++++++++++--- crates/owlry-core/src/providers/mod.rs | 6 ++- 2 files changed, 49 insertions(+), 8 deletions(-) diff --git a/crates/owlry-core/src/data/frecency.rs b/crates/owlry-core/src/data/frecency.rs index af43413..c968939 100644 --- a/crates/owlry-core/src/data/frecency.rs +++ b/crates/owlry-core/src/data/frecency.rs @@ -131,23 +131,36 @@ impl FrecencyStore { } } + /// Calculate frecency score using a pre-sampled timestamp. + /// Use this in hot loops to avoid repeated Utc::now() syscalls. + pub fn get_score_at(&self, item_id: &str, now: DateTime) -> f64 { + match self.data.entries.get(item_id) { + Some(entry) => Self::calculate_frecency_at(entry.launch_count, entry.last_launch, now), + None => 0.0, + } + } + /// Calculate frecency using Firefox-style algorithm fn calculate_frecency(launch_count: u32, last_launch: DateTime) -> f64 { let now = Utc::now(); + Self::calculate_frecency_at(launch_count, last_launch, now) + } + + /// Calculate frecency using a caller-provided timestamp. + fn calculate_frecency_at(launch_count: u32, last_launch: DateTime, now: DateTime) -> f64 { let age = now.signed_duration_since(last_launch); let age_days = age.num_hours() as f64 / 24.0; - // Recency weight based on how recently the item was used let recency_weight = if age_days < 1.0 { - 100.0 // Today + 100.0 } else if age_days < 7.0 { - 70.0 // This week + 70.0 } else if age_days < 30.0 { - 50.0 // This month + 50.0 } else if age_days < 90.0 { - 30.0 // This quarter + 30.0 } else { - 10.0 // Older + 10.0 }; launch_count as f64 * recency_weight @@ -206,6 +219,32 @@ mod tests { assert!(score_month < score_week); } + #[test] + fn get_score_at_matches_get_score() { + let mut store = FrecencyStore { + data: FrecencyData { + version: 1, + entries: HashMap::new(), + }, + path: PathBuf::from("/dev/null"), + dirty: false, + }; + store.data.entries.insert( + "test".to_string(), + FrecencyEntry { + launch_count: 5, + last_launch: Utc::now(), + }, + ); + + let now = Utc::now(); + let score_at = store.get_score_at("test", now); + let score = store.get_score("test"); + + // Both should be very close (same timestamp, within rounding) + assert!((score_at - score).abs() < 1.0); + } + #[test] fn test_launch_count_matters() { let now = Utc::now(); diff --git a/crates/owlry-core/src/providers/mod.rs b/crates/owlry-core/src/providers/mod.rs index fd607ec..8fc716f 100644 --- a/crates/owlry-core/src/providers/mod.rs +++ b/crates/owlry-core/src/providers/mod.rs @@ -16,6 +16,7 @@ pub use command::CommandProvider; // Re-export native provider for plugin loading pub use native_provider::NativeProvider; +use chrono::Utc; use fuzzy_matcher::FuzzyMatcher; use fuzzy_matcher::skim::SkimMatcherV2; use log::info; @@ -570,6 +571,7 @@ impl ProviderManager { query, max_results, frecency_weight ); + let now = Utc::now(); let mut results: Vec<(LaunchItem, i64)> = Vec::new(); // Add widget items first (highest priority) - only when: @@ -633,7 +635,7 @@ impl ProviderManager { } }) .map(|item| { - let frecency_score = frecency.get_score(&item.id); + let frecency_score = frecency.get_score_at(&item.id, now); let boosted = (frecency_score * frecency_weight * 100.0) as i64; (item, boosted) }) @@ -682,7 +684,7 @@ impl ProviderManager { }; base_score.map(|s| { - let frecency_score = frecency.get_score(&item.id); + let frecency_score = frecency.get_score_at(&item.id, now); let frecency_boost = (frecency_score * frecency_weight * 10.0) as i64; (item.clone(), s + frecency_boost) })