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,18 +8,56 @@
|
|||||||
|
|
||||||
use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, RefreshKind, System};
|
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<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
|
/// Snapshot the current process list's executable/command names. Lowercased
|
||||||
/// for case-insensitive pattern matching.
|
/// 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> {
|
pub fn list_running_process_names() -> Vec<String> {
|
||||||
let mut system = System::new_with_specifics(
|
ProcessLister::new().snapshot()
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Match a snapshot of process names against case-insensitive substring
|
/// Match a snapshot of process names against case-insensitive substring
|
||||||
|
|||||||
@@ -5,13 +5,46 @@
|
|||||||
//! that reminds the user to start recording with their hotkey. We do not
|
//! that reminds the user to start recording with their hotkey. We do not
|
||||||
//! start recording from this signal — the user decides.
|
//! 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]
|
#[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() {
|
if patterns.is_empty() {
|
||||||
return Ok(Vec::new());
|
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))
|
Ok(process_watch::match_meeting_patterns(&processes, &patterns))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -250,6 +250,7 @@ pub fn run() {
|
|||||||
app.manage(commands::audio::NativeCaptureState::new());
|
app.manage(commands::audio::NativeCaptureState::new());
|
||||||
app.manage(commands::live::LiveTranscriptionState::default());
|
app.manage(commands::live::LiveTranscriptionState::default());
|
||||||
app.manage(commands::tts::TtsState::new());
|
app.manage(commands::tts::TtsState::new());
|
||||||
|
app.manage(commands::meeting::MeetingState::new());
|
||||||
|
|
||||||
app.manage(AppState {
|
app.manage(AppState {
|
||||||
whisper_engine: Arc::new(LocalEngine::new(EngineName::new("whisper"))),
|
whisper_engine: Arc::new(LocalEngine::new(EngineName::new("whisper"))),
|
||||||
|
|||||||
Reference in New Issue
Block a user