From 3465f83a93edcd37147cdb4317c8889a2ca44871 Mon Sep 17 00:00:00 2001 From: Aaron Allen Date: Sat, 4 Apr 2026 10:47:05 -0500 Subject: [PATCH] perf(ops, cli): reduce avoidable allocations and extra sorting in the filter pipeline Skip the chronological pre-sort when no count limit is active, avoiding a redundant O(n log n) pass. In search matching, check note lines individually for exact/pattern modes to avoid joining into a temporary String allocation per entry. Closes #408 --- crates/ops/src/filter.rs | 21 +++++++++------------ crates/ops/src/search.rs | 21 ++++++++++++++++++--- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/crates/ops/src/filter.rs b/crates/ops/src/filter.rs index 224a667..54bf6be 100644 --- a/crates/ops/src/filter.rs +++ b/crates/ops/src/filter.rs @@ -98,20 +98,17 @@ pub fn filter_entries(mut entries: Vec, options: &FilterOptions) -> Vec, options: &FilterOptions) -> Vec { - // Sort chronologically for age selection (stable sort preserves insertion order for ties) - entries.sort_by_key(|a| a.date()); + if let Some(count) = options.count.filter(|&c| c < entries.len()) { + // Sort chronologically for age-based selection (stable sort preserves insertion order for ties) + entries.sort_by_key(|a| a.date()); - // Apply count limit based on age - if let Some(count) = options.count { let len = entries.len(); - if count < len { - match options.age.unwrap_or_default() { - Age::Newest => { - entries.drain(..len - count); - } - Age::Oldest => { - entries.truncate(count); - } + match options.age.unwrap_or_default() { + Age::Newest => { + entries.drain(..len - count); + } + Age::Oldest => { + entries.truncate(count); } } } diff --git a/crates/ops/src/search.rs b/crates/ops/src/search.rs index d6827e5..ed7b5dc 100644 --- a/crates/ops/src/search.rs +++ b/crates/ops/src/search.rs @@ -62,9 +62,24 @@ pub fn matches_entry(entry: &Entry, mode: &SearchMode, case: CaseSensitivity, in } if include_notes && !entry.note().is_empty() { - let note_text = entry.note().lines().join(" "); - if matches(¬e_text, mode, case) { - return true; + // Check each line individually to avoid joining into a temporary String. + // Fall back to a joined string only for modes that need cross-line matching. + let note = entry.note(); + match mode { + SearchMode::Regex(_) | SearchMode::Fuzzy(..) => { + // These modes may need to match across line boundaries + let note_text = note.lines().join(" "); + if matches(¬e_text, mode, case) { + return true; + } + } + _ => { + for line in note.lines() { + if matches(line, mode, case) { + return true; + } + } + } } } -- knotmirror 0.0.1