fix(local-asr): 切走/删模型/手动释放时真正归还 RAM

三个问题合修:

1. 用户点"释放"按钮后活动监视器看 RSS 不下降。原因:encoder f32 weights
   是 malloc 出来的几百 MB,free 后 macOS libmalloc 不会立即把页归还内核。
   修:drop QwenAsrEngine 后调 malloc_zone_pressure_relief(NULL, 0),主动
   归还。decoder bf16 走 mmap 不依赖这个。

2. 切回云端 ASR 后本地引擎仍驻留。set_active_asr_provider 切到非 local
   时未释放,1.2GB+ RAM 一直留到进程退出。

3. 删除已下载的模型时未释放内存里加载的同 id 引擎,留下 mmap 指向已
   unlink 文件 + RAM 不归还的双重问题。

Windows 端本期不上线(cache.rs macOS cfg 块包住改动)。
This commit is contained in:
baiqing committed 2026-05-05 20:20:46 +08:00
1 parent c85b1bd4c6
commit 463a6d9716
2 files changed
+52 -14

No files matched your search

@@ -94,29 +94,39 @@ impl LocalAsrCache {
pub fn release_if_idle(&self, idle_threshold: Duration) -> bool {
#[cfg(target_os = "macos")]
{
let mut slot = self.inner.lock();
if let Some(cached) = slot.as_ref() {
if cached.last_used.elapsed() >= idle_threshold {
log::info!(
"[local-asr cache] release engine {} after idle {:?}",
cached.model_id,
cached.last_used.elapsed()
);
slot.take();
return true;
let taken = {
let mut slot = self.inner.lock();
match slot.as_ref() {
Some(c) if c.last_used.elapsed() >= idle_threshold => {
log::info!(
"[local-asr cache] release engine {} after idle {:?}",
c.model_id,
c.last_used.elapsed()
);
slot.take()
}
_ => None,
}
};
if let Some(cached) = taken {
drop(cached);
pressure_relief_macos();
return true;
}
}
let _ = idle_threshold;
false
}
/// 立刻释放(用户点"立即释放"或删模型时调)。
/// 立刻释放(用户点"立即释放"、切走 provider、删模型时调)。
pub fn release_now(&self) {
#[cfg(target_os = "macos")]
{
if let Some(cached) = self.inner.lock().take() {
let taken = self.inner.lock().take();
if let Some(cached) = taken {
log::info!("[local-asr cache] release engine {} on demand", cached.model_id);
drop(cached);
pressure_relief_macos();
}
}
}
@@ -130,3 +140,18 @@ impl LocalAsrCache {
None
}
}
/// drop QwenAsrEngine 后调一次:让 macOS libmalloc 把 freelist 上的物理页归还内核。
/// 不调的话,encoder f32 weights 那 ~几百 MB 的 free 不会立刻反映到 RSS,活动监视器
/// 看起来"释放按钮没生效"。decoder bf16 走 mmap,munmap 时已立即生效,不依赖这个调用。
#[cfg(target_os = "macos")]
fn pressure_relief_macos() {
// SAFETY: 系统 API;NULL zone + goal=0 = 对所有 zone 尽量多地归还,无内存安全风险。
let freed = unsafe { malloc_zone_pressure_relief(std::ptr::null_mut(), 0) };
log::info!("[local-asr cache] malloc_zone_pressure_relief freed ~{} bytes", freed);
}
#[cfg(target_os = "macos")]
extern "C" {
fn malloc_zone_pressure_relief(zone: *mut libc::c_void, goal: libc::size_t) -> libc::size_t;
}
+15 -2
View File
@@ -123,9 +123,14 @@ pub fn set_credential(account: String, value: String) -> Result<(), String> {
#[tauri::command]
pub fn set_active_asr_provider(coord: CoordinatorState<'_>, provider: String) -> Result<(), String> {
CredentialsVault::set_active_asr_provider(&provider).map_err(|e| e.to_string())?;
// 切到本地 ASR → 后台预加载模型,下次按 hotkey 时不必等数秒。
if provider == crate::asr::local::PROVIDER_ID {
// 切到本地 ASR → 后台预加载模型,下次按 hotkey 时不必等数秒。
coord.preload_local_asr_in_background();
} else {
// 切回云端 → 用户已不需要本地引擎,立刻释放 1.2GB+ RAM;不释放的话只会等到
// schedule_local_asr_release 的下一次 dictation 才触发,而切回云端后根本不会
// 再走 local 路径,引擎会驻留到进程退出。
coord.release_local_asr_engine();
}
Ok(())
}
@@ -780,8 +785,16 @@ pub fn local_asr_cancel_download(
}
#[tauri::command]
pub fn local_asr_delete_model(model_id: String) -> Result<(), String> {
pub fn local_asr_delete_model(
coord: CoordinatorState<'_>,
model_id: String,
) -> Result<(), String> {
let id = ModelId::from_str(&model_id).ok_or_else(|| format!("unknown model id: {model_id}"))?;
// 如果内存里加载的就是要删的这个模型,先释放:否则 mmap 残留指向已 unlink 的文件,
// 且 RAM 直到下次切模型 / 用户手动按"释放"才回收。
if coord.local_asr_loaded_model().as_deref() == Some(id.as_str()) {
coord.release_local_asr_engine();
}
crate::asr::local::models::delete_model(id).map_err(|e| e.to_string())
}