From dc59aa92385fb02dd0a588a2cfc49d059d203385 Mon Sep 17 00:00:00 2001 From: Riz Ashraf Date: Mon, 21 Sep 2026 09:40:37 +0100 Subject: [PATCH] perf: eliminate loop string allocations and proxy mem spikes --- mcp-stdio/src/lib.rs | 3 ++- nvim-core/src/lib.rs | 4 ++-- server/src/handlers.rs | 35 ++++++++++++++++++++--------------- server/src/search.rs | 8 ++++---- stub/src/main.rs | 4 ++-- 5 files changed, 30 insertions(+), 24 deletions(-) diff --git a/mcp-stdio/src/lib.rs b/mcp-stdio/src/lib.rs index ca4d20d..3e9469f 100644 --- a/mcp-stdio/src/lib.rs +++ b/mcp-stdio/src/lib.rs @@ -6,8 +6,9 @@ pub async fn read_mcp_message( stdin: &mut BufReader, ) -> Option { let mut length = 0; + let mut line = String::new(); loop { - let mut line = String::new(); + line.clear(); if stdin.read_line(&mut line).await.unwrap_or(0) == 0 { return None; } diff --git a/nvim-core/src/lib.rs b/nvim-core/src/lib.rs index 16a85d8..f139ecf 100644 --- a/nvim-core/src/lib.rs +++ b/nvim-core/src/lib.rs @@ -37,9 +37,9 @@ pub async fn send_response(response: JsonRpcResponse) { // The MCP StdioTransport MUST use Newline-Delimited JSON (NDJSON). // Do NOT use LSP-style HTTP headers (e.g. Content-Length). // See MCP protocol spec (SEP-2575) and mcp-go-sdk bufio.Scanner implementation. - let payload = format!("{}\n", msg); let mut stdout = tokio::io::stdout(); - let _ = stdout.write_all(payload.as_bytes()).await; + let _ = stdout.write_all(msg.as_bytes()).await; + let _ = stdout.write_all(b"\n").await; let _ = stdout.flush().await; } diff --git a/server/src/handlers.rs b/server/src/handlers.rs index e555c04..1c0ba5a 100644 --- a/server/src/handlers.rs +++ b/server/src/handlers.rs @@ -2,6 +2,11 @@ use crate::models::*; use crate::state::MemoryState; use crate::tools::*; +fn contains_ignore_ascii_case(haystack: &str, needle: &str) -> bool { + if needle.is_empty() { return true; } + haystack.as_bytes().windows(needle.len()).any(|w| w.eq_ignore_ascii_case(needle.as_bytes())) +} + macro_rules! parse_tool { ($args:expr, $id:expr, $type:ty) => { match parse_args::<$type>($args) { @@ -605,8 +610,8 @@ impl MemoryHandler { continue; } if query.is_empty() - || name.to_lowercase().contains(&query) - || e.entity_type.to_lowercase().contains(&query) + || contains_ignore_ascii_case(&name, &query) + || contains_ignore_ascii_case(&e.entity_type, &query) { included.insert(name.clone()); } @@ -1014,9 +1019,9 @@ impl MemoryHandler { let snippets = self.state.snippets.read(); let mut results = Vec::new(); for s in snippets { - if s.name.to_lowercase().contains(&query) - || s.description.to_lowercase().contains(&query) - || s.language.to_lowercase().contains(&query) + if contains_ignore_ascii_case(&s.name, &query) + || contains_ignore_ascii_case(&s.description, &query) + || contains_ignore_ascii_case(&s.language, &query) { results.push(s); } @@ -1073,9 +1078,9 @@ impl MemoryHandler { if let Some(q) = req.query { let q = q.to_lowercase(); adrs.retain(|a| { - a.title.to_lowercase().contains(&q) - || a.context.to_lowercase().contains(&q) - || a.decision.to_lowercase().contains(&q) + contains_ignore_ascii_case(&a.title, &q) + || contains_ignore_ascii_case(&a.context, &q) + || contains_ignore_ascii_case(&a.decision, &q) }); } let data = serde_json::to_string(&adrs).unwrap_or_default(); @@ -1165,8 +1170,8 @@ impl MemoryHandler { let q = req.query.to_lowercase(); let mut fixes = self.state.error_fixes.read(); fixes.retain(|f| { - f.signature.to_lowercase().contains(&q) - || f.solution.to_lowercase().contains(&q) + contains_ignore_ascii_case(&f.signature, &q) + || contains_ignore_ascii_case(&f.solution, &q) }); let data = serde_json::to_string(&fixes).unwrap_or_default(); Ok(data.to_string()) @@ -1562,8 +1567,8 @@ impl MemoryHandler { .filter(|d| { (req.namespace.is_none() || d.namespace == *req.namespace.as_ref().unwrap()) - && (d.description.to_lowercase().contains(&q) - || d.ideal_solution.to_lowercase().contains(&q)) + && (contains_ignore_ascii_case(&d.description, &q) + || contains_ignore_ascii_case(&d.ideal_solution, &q)) }) .collect(); let memos: Vec<_> = self @@ -1574,7 +1579,7 @@ impl MemoryHandler { .filter(|m| { (req.namespace.is_none() || m.namespace == *req.namespace.as_ref().unwrap()) - && m.content.to_lowercase().contains(&q) + && contains_ignore_ascii_case(&m.content, &q) }) .collect(); let error_fixes: Vec<_> = self @@ -1583,8 +1588,8 @@ impl MemoryHandler { .read() .into_iter() .filter(|f| { - f.signature.to_lowercase().contains(&q) - || f.solution.to_lowercase().contains(&q) + contains_ignore_ascii_case(&f.signature, &q) + || contains_ignore_ascii_case(&f.solution, &q) }) .collect(); diff --git a/server/src/search.rs b/server/src/search.rs index cebf665..4ef1017 100644 --- a/server/src/search.rs +++ b/server/src/search.rs @@ -288,7 +288,7 @@ mod tests { namespace: "global".to_string(), git_branch: None, }; - index.index_entity(&entity).await.unwrap(); + let _ = index.index_entity(&entity).await.unwrap(); let task = Task { id: "task-1".to_string(), @@ -302,7 +302,7 @@ mod tests { dependencies: vec![], parent_id: None, }; - index.index_task(&task).await.unwrap(); + let _ = index.index_task(&task).await.unwrap(); let snippet = Snippet { name: "test_snippet".to_string(), @@ -311,7 +311,7 @@ mod tests { description: "A test snippet".to_string(), updated_at: 0, }; - index.index_snippet(&snippet).await.unwrap(); + let _ = index.index_snippet(&snippet).await.unwrap(); let adr = Adr { id: "adr-1".to_string(), @@ -321,7 +321,7 @@ mod tests { consequence: "Test consequence".to_string(), timestamp: 0, }; - index.index_adr(&adr).await.unwrap(); + let _ = index.index_adr(&adr).await.unwrap(); index.commit().await.unwrap(); index.reader.reload().unwrap(); diff --git a/stub/src/main.rs b/stub/src/main.rs index 1908e0e..14943cb 100644 --- a/stub/src/main.rs +++ b/stub/src/main.rs @@ -96,10 +96,10 @@ fn main() -> Result<(), Box> { if let tokio_tungstenite::tungstenite::Message::Text(text) = msg { let log_prefix = logger::extract_log_prefix(&text, true); tracing::info!("<<< [Stub] Received {} from server (length: {}): {}", log_prefix, text.len(), if text.len() > 1000 { format!("{}...", &text[..1000]) } else { text.clone() }); - let payload = format!("{}\n", text); use tokio::io::AsyncWriteExt; let mut stdout = tokio::io::stdout(); - let _ = stdout.write_all(payload.as_bytes()).await; + let _ = stdout.write_all(text.as_bytes()).await; + let _ = stdout.write_all(b"\n").await; let _ = stdout.flush().await; } }