diff --git a/crates/core/src/process_watch.rs b/crates/core/src/process_watch.rs index b9ab740..7a93552 100644 --- a/crates/core/src/process_watch.rs +++ b/crates/core/src/process_watch.rs @@ -8,18 +8,56 @@ use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, RefreshKind, System}; +/// Reusable wrapper around a `sysinfo::System` whose process table is +/// refreshed in place on every poll, instead of allocating a fresh one. +/// +/// On a busy host (~300 processes), `System::new_with_specifics` followed by +/// `refresh_processes` walks `/proc` cold and costs ~50–100 ms; reusing the +/// same instance reuses sysinfo's per-process bookkeeping so subsequent +/// refreshes are dominated by diffing rather than allocation. The Tauri +/// host holds one of these behind a `Mutex` for the meeting-detection +/// command to call every 15 s. +pub struct ProcessLister { + system: System, +} + +impl Default for ProcessLister { + fn default() -> Self { + Self::new() + } +} + +impl ProcessLister { + pub fn new() -> Self { + Self { + system: System::new_with_specifics( + RefreshKind::nothing().with_processes(ProcessRefreshKind::nothing()), + ), + } + } + + /// Refresh the process table in place and return the current + /// lowercased executable names. + pub fn snapshot(&mut self) -> Vec { + self.system + .refresh_processes(ProcessesToUpdate::All, true); + self.system + .processes() + .values() + .map(|process| process.name().to_string_lossy().to_lowercase()) + .collect() + } +} + /// Snapshot the current process list's executable/command names. Lowercased /// for case-insensitive pattern matching. +/// +/// Convenience wrapper that allocates a fresh `ProcessLister` per call. +/// Hot paths (the meeting-detection poller) should hold a long-lived +/// `ProcessLister` and call `snapshot()` directly to avoid the per-call +/// allocation of `System`'s internal bookkeeping. pub fn list_running_process_names() -> Vec { - let mut system = System::new_with_specifics( - RefreshKind::nothing().with_processes(ProcessRefreshKind::nothing()), - ); - system.refresh_processes(ProcessesToUpdate::All, true); - system - .processes() - .values() - .map(|process| process.name().to_string_lossy().to_lowercase()) - .collect() + ProcessLister::new().snapshot() } /// Match a snapshot of process names against case-insensitive substring diff --git a/src-tauri/src/commands/meeting.rs b/src-tauri/src/commands/meeting.rs index a73d22e..8d7e5cc 100644 --- a/src-tauri/src/commands/meeting.rs +++ b/src-tauri/src/commands/meeting.rs @@ -5,13 +5,46 @@ //! that reminds the user to start recording with their hotkey. We do not //! start recording from this signal — the user decides. -use kon_core::process_watch; +use std::sync::Mutex; + +use kon_core::process_watch::{self, ProcessLister}; + +/// Tauri-managed state for the meeting poller. Holds a long-lived +/// `ProcessLister` so each poll refreshes the existing `sysinfo::System` +/// in place instead of allocating a fresh one — the previous implementation +/// rebuilt the process table from scratch every 15 s. +pub struct MeetingState { + lister: Mutex, +} + +impl MeetingState { + pub fn new() -> Self { + Self { + lister: Mutex::new(ProcessLister::new()), + } + } +} + +impl Default for MeetingState { + fn default() -> Self { + Self::new() + } +} #[tauri::command] -pub fn detect_meeting_processes(patterns: Vec) -> Result, String> { +pub fn detect_meeting_processes( + state: tauri::State<'_, MeetingState>, + patterns: Vec, +) -> Result, String> { if patterns.is_empty() { return Ok(Vec::new()); } - let processes = process_watch::list_running_process_names(); + let processes = { + let mut lister = state + .lister + .lock() + .map_err(|e| format!("MeetingState lister poisoned: {e}"))?; + lister.snapshot() + }; Ok(process_watch::match_meeting_patterns(&processes, &patterns)) } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 6cd810c..19391cf 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -250,6 +250,7 @@ pub fn run() { app.manage(commands::audio::NativeCaptureState::new()); app.manage(commands::live::LiveTranscriptionState::default()); app.manage(commands::tts::TtsState::new()); + app.manage(commands::meeting::MeetingState::new()); app.manage(AppState { whisper_engine: Arc::new(LocalEngine::new(EngineName::new("whisper"))),