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:
@@ -8,19 +8,57 @@
|
||||
|
||||
use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, RefreshKind, System};
|
||||
|
||||
/// Snapshot the current process list's executable/command names. Lowercased
|
||||
/// for case-insensitive pattern matching.
|
||||
pub fn list_running_process_names() -> Vec<String> {
|
||||
let mut system = System::new_with_specifics(
|
||||
/// 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()),
|
||||
);
|
||||
system.refresh_processes(ProcessesToUpdate::All, true);
|
||||
system
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Refresh the process table in place and return the current
|
||||
/// lowercased executable names.
|
||||
pub fn snapshot(&mut self) -> Vec<String> {
|
||||
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<String> {
|
||||
ProcessLister::new().snapshot()
|
||||
}
|
||||
|
||||
/// Match a snapshot of process names against case-insensitive substring
|
||||
/// `patterns`. Returns the set of patterns that matched at least once, in
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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"))),
|
||||
|
||||
Reference in New Issue
Block a user