From 3d568148b8d755c1cbd9156264bc6283e15c741b Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 25 Apr 2026 08:46:57 +0000 Subject: [PATCH] perf(meeting): cache sysinfo System for the meeting-detection poller MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `detect_meeting_processes` is called every 15 s when meeting-auto-capture is enabled. The previous `list_running_process_names` allocated a fresh `sysinfo::System` per call and walked /proc cold; on a busy host (~300 processes) that's ~50–100 ms of work, every poll, forever. Add `kon_core::process_watch::ProcessLister`, a thin wrapper around a long-lived `System` whose process table is refreshed in place. The Tauri host holds one behind a `Mutex` in a new `MeetingState` managed at app setup. The free `list_running_process_names` is kept as a convenience that constructs a fresh `ProcessLister` per call — its only remaining caller is the existing smoke test. - ProcessLister + Default in crates/core/src/process_watch.rs. - MeetingState in src-tauri/src/commands/meeting.rs; the command takes it via `tauri::State` and locks for the duration of the snapshot. - src-tauri/src/lib.rs registers MeetingState alongside the other managed states. https://claude.ai/code/session_0189xUb6ie6t9qHkzatGZ9Rb --- crates/core/src/process_watch.rs | 56 ++++++++++++++++++++++++++----- src-tauri/src/commands/meeting.rs | 39 +++++++++++++++++++-- src-tauri/src/lib.rs | 1 + 3 files changed, 84 insertions(+), 12 deletions(-) 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"))),