Skip to content

Commit dccaf88

Browse files
committed
perf: fix critical power usage hotspots
- Add 300ms debounce to context indexer file watcher (was unbounded) - Increase watcher poll interval to 2s (was default/fast) - Increase memory flusher interval from 10s to 30s - Cache tree-sitter parsers per language (was re-creating 2000x per cycle) - Disable auto cargo check in Kairos tick (was running every 60s) - Increase Kairos tick interval from 60s to 300s - Fix Windows working set trim: 512MB/1GB instead of MAX/MAX (was causing page thrashing) - Add missing websearch module to mod.rs
1 parent 42404f5 commit dccaf88

6 files changed

Lines changed: 111 additions & 57 deletions

File tree

src-tauri/src/domain/indexing/context_indexer.rs

Lines changed: 96 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ impl ContextIndexer {
7474
if let Ok(event) = res {
7575
let _ = tx.blocking_send(event);
7676
}
77-
}, notify::Config::default());
77+
}, notify::Config::default().with_poll_interval(std::time::Duration::from_secs(2)));
7878

7979
if let Ok(mut watcher) = watcher_res {
8080
if let Err(e) = watcher.watch(&root, RecursiveMode::Recursive) {
@@ -85,28 +85,66 @@ impl ContextIndexer {
8585
// Keep watcher alive in this thread
8686
let _watcher = watcher;
8787

88-
while let Some(event) = rx.recv().await {
89-
match event.kind {
90-
EventKind::Modify(_) | EventKind::Create(_) => {
91-
// If the user edited `.cursorignore`, refresh
92-
// the cached matcher so subsequent events
93-
// honor the new rules immediately.
94-
if event.paths.iter().any(|p| p.file_name().map(|n| n == ".hadesignore" || n == ".cursorignore" || n == ".cursorindexignore" || n == ".gitignore").unwrap_or(false)) {
95-
let fresh = IgnoreSet::load(&root);
96-
if let Ok(mut w) = ignore_set.write() {
97-
*w = fresh;
88+
// Debounce: batch rapid-fire events (e.g. during builds)
89+
let mut pending_paths: Vec<std::path::PathBuf> = Vec::new();
90+
let mut debounce_deadline = None;
91+
92+
loop {
93+
// If we have pending events and the debounce window expired, process them
94+
if !pending_paths.is_empty() {
95+
if let Some(deadline) = debounce_deadline {
96+
if tokio::time::Instant::now() >= deadline {
97+
let paths: Vec<_> = pending_paths.drain(..).collect();
98+
debounce_deadline = None;
99+
let snapshot = ignore_set.read().ok().map(|g| g.clone());
100+
for path in paths {
101+
if Self::is_indexable_with(&path, snapshot.as_ref()) {
102+
if let Err(e) = Self::index_single_file(&ms, &root, &path).await {
103+
eprintln!("[CONTEXT] Error indexing file {:?}: {:?}", path, e);
104+
}
105+
}
98106
}
107+
continue;
99108
}
100-
let snapshot = ignore_set.read().ok().map(|g| g.clone());
101-
for path in event.paths {
102-
if Self::is_indexable_with(&path, snapshot.as_ref()) {
103-
if let Err(e) = Self::index_single_file(&ms, &root, &path).await {
104-
eprintln!("[CONTEXT] Error indexing file {:?}: {:?}", path, e);
109+
}
110+
}
111+
112+
// Wait for next event or timeout
113+
let timeout = debounce_deadline.map(|d| {
114+
let remaining = d.saturating_duration_since(tokio::time::Instant::now());
115+
remaining.min(std::time::Duration::from_millis(500))
116+
}).unwrap_or(std::time::Duration::from_millis(500));
117+
118+
match tokio::time::timeout(timeout, rx.recv()).await {
119+
Ok(Some(event)) => {
120+
match event.kind {
121+
EventKind::Modify(_) | EventKind::Create(_) => {
122+
// Refresh ignore files
123+
if event.paths.iter().any(|p| p.file_name().map(|n| n == ".hadesignore" || n == ".cursorignore" || n == ".cursorindexignore" || n == ".gitignore").unwrap_or(false)) {
124+
let fresh = IgnoreSet::load(&root);
125+
if let Ok(mut w) = ignore_set.write() {
126+
*w = fresh;
127+
}
105128
}
129+
// Collect paths for debounced processing
130+
for path in event.paths {
131+
if path.is_file() {
132+
pending_paths.push(path);
133+
}
134+
}
135+
// Set/reset debounce deadline (300ms window)
136+
debounce_deadline = Some(tokio::time::Instant::now() + std::time::Duration::from_millis(300));
106137
}
138+
_ => {}
139+
}
140+
}
141+
Ok(None) => break, // Channel closed
142+
Err(_) => {
143+
// Timeout — process any pending events
144+
if !pending_paths.is_empty() {
145+
debounce_deadline = Some(tokio::time::Instant::now());
107146
}
108147
}
109-
_ => {}
110148
}
111149
}
112150
});
@@ -422,42 +460,51 @@ impl ContextIndexer {
422460

423461
fn extract_symbols_detailed(content: &str, ext: &str, path: &str) -> Vec<crate::memory_store::SymbolDefinition> {
424462
let mut symbols = Vec::new();
425-
let mut parser = Parser::new();
426463

427-
let language = match ext {
428-
"rs" => tree_sitter_rust::LANGUAGE,
429-
"ts" | "tsx" => tree_sitter_typescript::LANGUAGE_TYPESCRIPT,
430-
"js" | "jsx" => tree_sitter_typescript::LANGUAGE_TSX,
431-
"py" => tree_sitter_python::LANGUAGE,
464+
let (language, query_str) = match ext {
465+
"rs" => (tree_sitter_rust::LANGUAGE.into(),
466+
"(function_item name: (identifier) @name) @kind_func
467+
(struct_item name: (type_identifier) @name) @kind_struct
468+
(enum_item name: (type_identifier) @name) @kind_enum
469+
(trait_item name: (type_identifier) @name) @kind_trait
470+
(impl_item type: (type_identifier) @name) @kind_impl"),
471+
"ts" | "tsx" | "js" | "jsx" => (tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
472+
"(function_declaration name: (identifier) @name) @kind_func
473+
(variable_declarator name: (identifier) @name value: (arrow_function)) @kind_func
474+
(method_definition name: (property_identifier) @name) @kind_func
475+
(class_declaration name: (type_identifier) @name) @kind_class
476+
(interface_declaration name: (type_identifier) @name) @kind_interface
477+
(type_alias_declaration name: (type_identifier) @name) @kind_type"),
478+
"py" => (tree_sitter_python::LANGUAGE.into(),
479+
"(function_definition name: (identifier) @name) @kind_func
480+
(class_definition name: (identifier) @name) @kind_class"),
432481
_ => return Vec::new(),
433482
};
434483

435-
parser.set_language(&language.into()).expect("Error loading language");
436-
let tree = parser.parse(content, None).expect("Error parsing code");
437-
438-
let query_str = match ext {
439-
"rs" => "(function_item name: (identifier) @name) @kind_func
440-
(struct_item name: (type_identifier) @name) @kind_struct
441-
(enum_item name: (type_identifier) @name) @kind_enum
442-
(trait_item name: (type_identifier) @name) @kind_trait
443-
(impl_item type: (type_identifier) @name) @kind_impl",
444-
"ts" | "tsx" | "js" | "jsx" => "(function_declaration name: (identifier) @name) @kind_func
445-
(variable_declarator name: (identifier) @name value: (arrow_function)) @kind_func
446-
(method_definition name: (property_identifier) @name) @kind_func
447-
(class_declaration name: (type_identifier) @name) @kind_class
448-
(interface_declaration name: (type_identifier) @name) @kind_interface
449-
(type_alias_declaration name: (type_identifier) @name) @kind_type",
450-
"py" => "(function_definition name: (identifier) @name) @kind_func
451-
(class_definition name: (identifier) @name) @kind_class",
452-
_ => "",
453-
};
454-
455-
if query_str.is_empty() {
456-
return Vec::new();
484+
// Cache parser per language using thread_local to avoid re-creating 2000x per cycle
485+
use std::cell::RefCell;
486+
thread_local! {
487+
static PARSER_CACHE: RefCell<HashMap<String, Parser>> = RefCell::new(HashMap::new());
457488
}
458-
459-
let query = Query::new(&language.into(), query_str).expect("Error creating query");
460-
let mut cursor = QueryCursor::new();
489+
490+
PARSER_CACHE.with(|cache| {
491+
let mut cache = cache.borrow_mut();
492+
let parser = cache.entry(ext.to_string()).or_insert_with(|| {
493+
let mut p = Parser::new();
494+
p.set_language(&language).expect("Error loading language");
495+
p
496+
});
497+
498+
let tree = match parser.parse(content, None) {
499+
Some(t) => t,
500+
None => return Vec::new(),
501+
};
502+
503+
let query = match Query::new(&language, query_str) {
504+
Ok(q) => q,
505+
Err(_) => return Vec::new(),
506+
};
507+
let mut cursor = QueryCursor::new();
461508
let mut matches = cursor.matches(&query, tree.root_node(), content.as_bytes());
462509
while let Some(m) = StreamingIterator::next(&mut matches) {
463510
let mut name = String::new();
@@ -487,6 +534,7 @@ impl ContextIndexer {
487534
}
488535
}
489536
symbols
537+
})
490538
}
491539

492540
/// Returns just the symbol names for a file — useful for quick context summaries.

src-tauri/src/domain/memory/memory_store.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,8 @@ impl MemoryStore {
157157
if !FLUSHER_ACTIVE.swap(true, Ordering::SeqCst) {
158158
tauri::async_runtime::spawn(async move {
159159
loop {
160-
// Optimized Flush Interval: 10 seconds for performance balance
161-
tokio::time::sleep(tokio::time::Duration::from_secs(10)).await;
160+
// Flush every 30s (was 10s) to reduce CPU/RAM churn from deep-cloning
161+
tokio::time::sleep(tokio::time::Duration::from_secs(30)).await;
162162

163163
if dirty.load(Ordering::SeqCst) {
164164
// Silent persistence to protect logs; only error if I/O fails

src-tauri/src/domain/tools/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ pub mod shell;
1515
pub mod terminal_tools;
1616
pub mod web_edit;
1717
pub mod web_tools;
18+
pub mod websearch;
1819
pub mod workflow_tools;
1920

2021
pub use registry::{AiTools, ToolDefinition};

src-tauri/src/domain/workspace/kairos.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,9 @@ impl KairosEngine {
7575
let _ = self.indexer.reindex_if_needed(&root);
7676

7777
// 2. "Dreaming" — Proactive Diagnostics on idle
78-
if root.join("Cargo.toml").exists() {
78+
// DISABLED: cargo check every 60s was saturating CPU on large projects.
79+
// Diagnostics are now available on-demand via dev_cargo_diagnostics tool.
80+
if false && root.join("Cargo.toml").exists() {
7981
println!("[KAIROS] Dreaming: Running cargo diagnostics...");
8082
self.emit_suggestion("Indexing", "Kairos is deep-scanning project symbols in parallel...");
8183

src-tauri/src/lib.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -303,16 +303,19 @@ pub fn run() {
303303
let _ = SetProcessWorkingSetSize(handle, usize::MAX, usize::MAX);
304304
}
305305

306-
// Periodic working set trim: reclaim unused pages every 5 minutes.
306+
// Periodic working set trim: reclaim unused pages every 10 minutes.
307307
// After heavy indexing or long agent loops, Rust may hold large amounts
308-
// of paged-out heap. This forces Windows to reclaim those pages.
308+
// of paged-out heap. Use moderate values to avoid page thrashing.
309309
#[cfg(target_os = "windows")]
310310
tauri::async_runtime::spawn(async {
311311
loop {
312-
tokio::time::sleep(tokio::time::Duration::from_secs(300)).await;
312+
tokio::time::sleep(tokio::time::Duration::from_secs(600)).await;
313313
unsafe {
314314
let handle = GetCurrentProcess();
315-
let _ = SetProcessWorkingSetSize(handle, usize::MAX, usize::MAX);
315+
// Use 512MB min / 1GB max instead of MAX/MAX to avoid thrashing
316+
let min_bytes = 512 * 1024 * 1024;
317+
let max_bytes = 1024 * 1024 * 1024;
318+
let _ = SetProcessWorkingSetSize(handle, min_bytes, max_bytes);
316319
}
317320
}
318321
});

src-tauri/src/state.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -434,9 +434,9 @@ impl EditorState {
434434
let k_clone = kairos.clone();
435435
tauri::async_runtime::spawn(async move {
436436
loop {
437-
// 60s: was 10s, which spawned `cargo` every 10s and held
437+
// 300s: was 60s, which spawned `cargo` every minute and held
438438
// the process at "High" GPU/CPU power even when idle.
439-
tokio::time::sleep(tokio::time::Duration::from_secs(60)).await;
439+
tokio::time::sleep(tokio::time::Duration::from_secs(300)).await;
440440
k_clone.tick().await;
441441
}
442442
});

0 commit comments

Comments
 (0)