Skip to content

perf(local-asr): 引擎状态改后端事件驱动,前端零轮询#672

Merged
appergb merged 2 commits into
betafrom
fix/470-localasr-engine-event
Jun 16, 2026
Merged

perf(local-asr): 引擎状态改后端事件驱动,前端零轮询#672
appergb merged 2 commits into
betafrom
fix/470-localasr-engine-event

Conversation

@appergb

@appergb appergb commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

User description

源自 #470 Cloud 性能审查 #6。LocalAsr 前端原本 setInterval 每 5s 轮询 refreshEngineStatus

改动:后端在每个状态转换点主动 emit local-asr:engine-changed(load、idle 释放 release_if_idle==true、手动释放汇聚点、keepLoadedSecs 变更),前端 listen 增量更新、删掉 5s 轮询与 engineStatusTimer。挂载时仍拉一次初值,cleanup unlistenLocalAsrEngineStatusClone。helper 只用跨平台符号,不碰 Foundry/Sherpa。

验证:macOS cargo check + npm run build 通过。


PR Type

Enhancement


Description

  • 后端主动推送引擎状态事件

  • 前端零轮询,监听事件更新

  • 跨平台兼容(Android 空实现)


Diagram Walkthrough

flowchart LR
    subgraph Backend [Backend Rust]
        coordinator["coordinator.rs"]
    end
    subgraph Frontend [Frontend React]
        localAsr["LocalAsr.tsx"]
    end
    coordinator -- "emit 'local-asr:engine-changed'" --> localAsr
Loading

File Walkthrough

Relevant files
Enhancement
local_asr.rs
派生 Clone 并触发状态推送                                                                                 

openless-all/app/src-tauri/src/commands/local_asr.rs

  • 为 LocalAsrEngineStatus 添加 Clone 派生
  • local_asr_set_keep_loaded_secs 执行后调用 emit 通知前端
+4/-2     
coordinator.rs
实现事件发射函数与跨平台适配                                                                                     

openless-all/app/src-tauri/src/coordinator.rs

  • 新增 emit_local_asr_engine_status 函数
  • 在引擎加载/释放/keepLoadedSecs 变更时调用
  • 添加 Android 编译空实现(no-op stub)
+38/-1   
LocalAsr.tsx
前端零轮询监听引擎状态                                                                                           

openless-all/app/src/pages/LocalAsr.tsx

  • 移除 engineStatusTimer 轮询及清理代码
  • 添加 useEffect 监听 local-asr:engine-changed 事件
  • 预加载后不再手动延迟刷新状态
+32/-10 

issue #470:LocalAsr 前端原本 setInterval 每 5s 轮询 refreshEngineStatus。改为后端在每个
状态转换点主动 emit `local-asr:engine-changed`(load: build_local_qwen3 / preload;idle
释放: schedule_local_asr_release 的 release_if_idle==true;手动释放: release_local_asr_engine
汇聚点;keepLoadedSecs 变更),前端 listen 该事件增量更新、删掉 5s 轮询与 engineStatusTimer。
挂载时仍拉一次初值,listener 在 cleanup unlisten。LocalAsrEngineStatus 加 Clone 供 emit 复用。
helper 只用跨平台符号,不碰 Foundry/Sherpa。
@appergb

appergb commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator Author

@claude 请审查本 PR(#470 #6:本地 ASR 引擎状态改后端事件驱动)。重点核查 emit 覆盖完整性(load/idle 释放/手动释放/keepLoadedSecs 五个转换点)、idle 释放闭包 inner Arc 是否正确、前端 listener cleanup unlisten 是否到位、删轮询后无悬空引用、helper 是否只用跨平台符号不误碰 Foundry/Sherpa。

@github-actions

github-actions Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

PR Reviewer Guide 🔍

(Review updated until commit 130257f)

Here are some key observations to aid the review process:

🎫 Ticket compliance analysis 🔶

6 - Partially compliant

Compliant requirements:

  • 性能优化(第5点):将前端轮询改为后端事件驱动,消除无谓的 5s 轮询,降低开销

Non-compliant requirements:

  • 右键 option 兼容模式
  • 直接输入功能
  • 不调用大模型开关
  • 正则表达优化
  • 简化配置
  • 本地模型下载选项
  • 光标不插入文本的 bug

Requires further human verification:

470 - Partially compliant

Compliant requirements:

Non-compliant requirements:

  • 未直接修复语音胶囊显示问题

Requires further human verification:

  • 语音胶囊显示可能依赖于引擎状态事件推送,此 PR 虽改进状态通知机制,但胶囊是否正常显示需人工在 Windows 环境验证
⏱️ Estimated effort to review: 2 🔵🔵⚪⚪⚪
🧪 No relevant tests
🔒 No security concerns identified
⚡ No major issues detected

@claude

claude Bot commented Jun 15, 2026

Copy link
Copy Markdown

Claude finished @appergb's task in 4m 12s —— View job


PR #672 代码审查

任务清单

  • 加载工具
  • 读取变更文件(coordinator.rs / local_asr.rs / LocalAsr.tsx)
  • 核查 emit 覆盖完整性(5个转换点)
  • 核查 idle 释放闭包 inner Arc 正确性
  • 核查前端 listener cleanup/unlisten
  • 核查删轮询后无悬空引用
  • 核查 helper 跨平台符号(不碰 Foundry/Sherpa)

总体评价

实现正确,逻辑清晰,可以合并。 所有关键点均通过审查,下方为逐项结论与两个可选改进建议。


1. emit 覆盖完整性 ✅

覆盖了所有会改变引擎状态的路径:

转换点 调用位置 状态
load(QA dictation 触发) build_local_qwen3 末尾
load(用户手动 preload) preload_local_asr_in_background spawn 完成后
idle 释放 keep_secs==0 schedule_local_asr_release 同步分支
idle 释放 release_if_idle==true schedule_local_asr_release async 分支内
手动释放汇聚点 release_local_asr_engine
keepLoadedSecs 变更 local_asr_set_keep_loaded_secs

release_if_idle 返回 false 时不 emit,这是正确行为——引擎仍在加载状态,状态未变。


2. idle 释放闭包 inner Arc ✅

schedule_local_asr_release 中的修改:

// coordinator.rs:3262
let inner = Arc::clone(inner);  // 新增:用 owned Arc 遮蔽 &Arc 引用
tauri::async_runtime::spawn(async move {
    tokio::time::sleep(dur).await;
    if cache.release_if_idle(dur) {
        emit_local_asr_engine_status(&inner);  // 借用 move 进来的 Arc
    }
});

let inner = Arc::clone(inner)&Arc<Inner> 转为 owned Arc<Inner>,遮蔽参数名后由 async move 按值捕获。这是标准的 Rust Arc 生命周期延长惯用法,引用计数和 drop 时机均正确。


3. 前端 listener cleanup/unlisten ✅

// LocalAsr.tsx:517-543
useEffect(() => {
    if (!isTauri) return
    void refreshEngineStatus()
    let unlisten: undefined | (() => void)
    let cancelled = false
    ;(async () => {
        const off = await listen<LocalAsrEngineStatus>("local-asr:engine-changed", ...)
        if (cancelled) { off() }   // 竞态:组件在 listen 返回前已卸载
        else { unlisten = off }
    })()...
    return () => {
        cancelled = true
        if (unlisten) unlisten()   // 正常路径
    }
}, [])

竞态处理完整:

  • 卸载发生在 listen resolve 之前:cleanup 先设 cancelled = true,async 完成后发现 cancelled 立即调 off()
  • 卸载发生在 listen resolve 之后:cleanup 直接调 unlisten()
  • 非 Tauri 环境:return early,cleanup 返回 undefined(React 忽略)✅

与同文件 local-asr-download-progressfoundry-local-asr-prepare-progress 等监听模式一致。


4. 删轮询后无悬空引用 ✅

  • engineStatusTimer ref 已删除 ✅
  • window.setInterval / window.clearInterval 已清除 ✅
  • scrollGuardCleanup.current cleanup 保留在第一个 useEffect 的 return 中 ✅
  • handlePreload 中的 window.setTimeout(() => refreshEngineStatus(), 1500) 已删除(由 backend emit 替代)✅
  • refreshEngineStatus 函数保留(初始拉值 + handleReleaseEngine 防御性调用)✅

5. helper 跨平台符号 ✅

// coordinator.rs:3237-3248
fn emit_local_asr_engine_status(inner: &Arc<Inner>) {
    let model_id = inner.local_asr_cache.loaded_model_id();
    let keep_loaded_secs = inner.prefs.get().local_asr_keep_loaded_secs;
    let status = crate::commands::LocalAsrEngineStatus { loaded: model_id.is_some(), model_id, keep_loaded_secs };
    if let Some(app) = inner.app.lock().clone() {
        let _ = app.emit("local-asr:engine-changed", &status);
    }
}
  • local_asr_cacheprefsapp 均为 Inner 上不带 #[cfg] guard 的字段(与已有 release_local_asr_engine 同路径)✅
  • LocalAsrEngineStatus 仅含 bool / Option<String> / u32,无平台差异 ✅
  • 无任何 FoundryLocalRuntime / SherpaDownloadManager / SherpaOnnxRuntime 符号 ✅
  • app.emit() 是 Tauri 跨平台 API ✅
  • 函数本身无 #[cfg] gate,可在 Windows/Linux 上编译 ✅

可选改进(非阻塞)

handleReleaseEnginerefreshEngineStatus() 现已冗余

// LocalAsr.tsx:1378-1385
const handleReleaseEngine = async () => {
    try {
        await releaseLocalAsrEngine()
        await refreshEngineStatus()  // backend 已 emit,此 IPC 拉取冗余
    } catch (e) { ... }
}

releaseLocalAsrEngine() 后端会 emit local-asr:engine-changed,listener 已更新状态,此处再拉一次会造成双重 setState(结果一致,无 bug,但有 1 次多余 IPC)。Fix this →

handleKeepLoadedChange 通过 refresh() 间接重拉引擎状态

const handleKeepLoadedChange = async (seconds: number) => {
    await setLocalAsrKeepLoadedSecs(seconds)
    await refresh()  // refresh() 内含 refreshEngineStatus(),同上冗余
}

可改为仅刷新 settings/models,不重复拉引擎状态。同样无 bug,属于性能上的微小冗余。


结论:PR 实现正确,两个冗余 IPC 属可选优化,不影响合并。

@HKLHaoBin

Copy link
Copy Markdown
Contributor

CI 这边 Android cargo check 挂了,阻塞合并。Windows / macOS / Linux 都过了。

失败点

error[E0422]: cannot find struct, variant or union type `LocalAsrEngineStatus` in module `crate::commands`
  --> src/coordinator.rs:3240:35

根因(看起来是)
新增的 emit_local_asr_engine_statuscoordinator.rs 里直接构造了 crate::commands::LocalAsrEngineStatus,但 commands/mod.rslocal_asr 模块是 #[cfg(not(mobile))] 导出的——Android 目标下 crate::commands 里没有这个类型。coordinator 侧的 local_asr_cache / schedule_local_asr_release 等则在全平台都会编译,所以 Android CI 会在这里炸。

修复方向(任选其一即可)

  1. 把 emit payload 的类型挪到 mobile 也会编译的模块(例如 types.rs),coordinator 和 commands 共用;
  2. 在 coordinator 里定义一个私有小 struct(字段同 LocalAsrEngineStatus),emit 时用 serde 对齐前端即可;
  3. emit_local_asr_engine_status 及其调用点加 #[cfg(not(mobile))](Android 本来也没有 LocalAsr 页面,事件驱动可以只在 desktop 启用)。

修完后麻烦 push 一下,我这边会再看 Android cargo check 是否变绿。Thanks!

CI Android cargo check 失败:LocalAsrEngineStatus 不在 android target 编译。给 helper 加
#[cfg(not(target_os="android"))] 真实现 + #[cfg(target_os="android")] no-op stub,
各调用点在所有平台统一编译。issue #470 / #6
@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 130257f

@appergb appergb merged commit 001615e into beta Jun 16, 2026
5 checks passed
@appergb appergb deleted the fix/470-localasr-engine-event branch June 16, 2026 03:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants