perf(meeting): cache sysinfo System for the meeting-detection poller

`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<ProcessLister>` 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
This commit is contained in:
Claude
2026-04-25 08:46:57 +00:00
parent f3fd86185e
commit 3d568148b8
3 changed files with 84 additions and 12 deletions

View File

@@ -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<ProcessLister>,
}
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<String>) -> Result<Vec<String>, String> {
pub fn detect_meeting_processes(
state: tauri::State<'_, MeetingState>,
patterns: Vec<String>,
) -> Result<Vec<String>, 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))
}

View File

@@ -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"))),