fix: auto-unload model after idle timeout to reduce memory (#1051)
- Default model_unload_timeout from Never to Min5 - Fix Drop impl: use take() on watcher handle so clones from initiate_model_load() don't kill the watcher thread - Reset last_activity on model load to prevent immediate unload - Upgrade watcher logging from debug to info level - Remove duplicate "unloaded" event (unload_model already emits it) Co-authored-by: CJ Pais <cj@cjpais.com>
This commit is contained in:
parent
789a4393e8
commit
d1da935479
2 changed files with 40 additions and 21 deletions
|
|
@ -97,6 +97,7 @@ impl TranscriptionManager {
|
||||||
let manager_cloned = manager.clone();
|
let manager_cloned = manager.clone();
|
||||||
let shutdown_signal = manager.shutdown_signal.clone();
|
let shutdown_signal = manager.shutdown_signal.clone();
|
||||||
let handle = thread::spawn(move || {
|
let handle = thread::spawn(move || {
|
||||||
|
debug!("Idle watcher thread started");
|
||||||
while !shutdown_signal.load(Ordering::Relaxed) {
|
while !shutdown_signal.load(Ordering::Relaxed) {
|
||||||
thread::sleep(Duration::from_secs(10)); // Check every 10 seconds
|
thread::sleep(Duration::from_secs(10)); // Check every 10 seconds
|
||||||
|
|
||||||
|
|
@ -114,28 +115,29 @@ impl TranscriptionManager {
|
||||||
.duration_since(SystemTime::UNIX_EPOCH)
|
.duration_since(SystemTime::UNIX_EPOCH)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.as_millis() as u64;
|
.as_millis() as u64;
|
||||||
|
let idle_ms = now_ms.saturating_sub(last);
|
||||||
|
let limit_ms = limit_seconds * 1000;
|
||||||
|
|
||||||
if now_ms.saturating_sub(last) > limit_seconds * 1000 {
|
if idle_ms > limit_ms {
|
||||||
// idle -> unload
|
// idle -> unload
|
||||||
if manager_cloned.is_model_loaded() {
|
if manager_cloned.is_model_loaded() {
|
||||||
let unload_start = std::time::Instant::now();
|
let unload_start = std::time::Instant::now();
|
||||||
debug!("Starting to unload model due to inactivity");
|
info!(
|
||||||
|
"Model idle for {}s (limit: {}s), unloading",
|
||||||
if let Ok(()) = manager_cloned.unload_model() {
|
idle_ms / 1000,
|
||||||
let _ = app_handle_cloned.emit(
|
limit_seconds
|
||||||
"model-state-changed",
|
);
|
||||||
ModelStateEvent {
|
match manager_cloned.unload_model() {
|
||||||
event_type: "unloaded".to_string(),
|
Ok(()) => {
|
||||||
model_id: None,
|
let unload_duration = unload_start.elapsed();
|
||||||
model_name: None,
|
info!(
|
||||||
error: None,
|
"Model unloaded due to inactivity (took {}ms)",
|
||||||
},
|
unload_duration.as_millis()
|
||||||
);
|
);
|
||||||
let unload_duration = unload_start.elapsed();
|
}
|
||||||
debug!(
|
Err(e) => {
|
||||||
"Model unloaded due to inactivity (took {}ms)",
|
error!("Failed to unload idle model: {}", e);
|
||||||
unload_duration.as_millis()
|
}
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -399,6 +401,15 @@ impl TranscriptionManager {
|
||||||
*current_model = Some(model_id.to_string());
|
*current_model = Some(model_id.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset idle timer so the watcher doesn't immediately unload a just-loaded model
|
||||||
|
self.last_activity.store(
|
||||||
|
SystemTime::now()
|
||||||
|
.duration_since(SystemTime::UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_millis() as u64,
|
||||||
|
Ordering::Relaxed,
|
||||||
|
);
|
||||||
|
|
||||||
// Emit loading completed event
|
// Emit loading completed event
|
||||||
let _ = self.app_handle.emit(
|
let _ = self.app_handle.emit(
|
||||||
"model-state-changed",
|
"model-state-changed",
|
||||||
|
|
@ -771,7 +782,15 @@ pub fn get_available_accelerators() -> AvailableAccelerators {
|
||||||
|
|
||||||
impl Drop for TranscriptionManager {
|
impl Drop for TranscriptionManager {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
debug!("Shutting down TranscriptionManager");
|
// Skip shutdown unless this is the very last clone. TranscriptionManager
|
||||||
|
// is cloned by initiate_model_load() and the watcher thread — those
|
||||||
|
// clones dropping must not kill the watcher. The watcher thread holds
|
||||||
|
// its own clone, so engine's strong_count is always >= 2 while the
|
||||||
|
// watcher is alive. When it reaches 1, only this instance remains
|
||||||
|
// and we can safely shut down.
|
||||||
|
if Arc::strong_count(&self.engine) > 1 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Signal the watcher thread to shutdown
|
// Signal the watcher thread to shutdown
|
||||||
self.shutdown_signal.store(true, Ordering::Relaxed);
|
self.shutdown_signal.store(true, Ordering::Relaxed);
|
||||||
|
|
|
||||||
|
|
@ -180,7 +180,7 @@ impl Default for KeyboardImplementation {
|
||||||
|
|
||||||
impl Default for ModelUnloadTimeout {
|
impl Default for ModelUnloadTimeout {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
ModelUnloadTimeout::Never
|
ModelUnloadTimeout::Min5
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -735,7 +735,7 @@ pub fn get_default_settings() -> AppSettings {
|
||||||
debug_mode: false,
|
debug_mode: false,
|
||||||
log_level: default_log_level(),
|
log_level: default_log_level(),
|
||||||
custom_words: Vec::new(),
|
custom_words: Vec::new(),
|
||||||
model_unload_timeout: ModelUnloadTimeout::Never,
|
model_unload_timeout: ModelUnloadTimeout::default(),
|
||||||
word_correction_threshold: default_word_correction_threshold(),
|
word_correction_threshold: default_word_correction_threshold(),
|
||||||
history_limit: default_history_limit(),
|
history_limit: default_history_limit(),
|
||||||
recording_retention_period: default_recording_retention_period(),
|
recording_retention_period: default_recording_retention_period(),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue