fix(ohos): window-state try_lock to prevent appfreeze on resize/move [urgent] - #24
fix(ohos): window-state try_lock to prevent appfreeze on resize/move [urgent]#24ddxwzc-boop wants to merge 1 commit into
Conversation
OHOS PC 上 examples/api resize 窗口时高频触发 appfreeze(THREAD_BLOCK_6S), 系统看门狗杀进程。2026-08-13 抓 faultlog + llvm-addr2line 符号化确认死点。 根因(AB-BA 死锁): - save_window_state 持有 cache.lock(),随后做同步 NAPI 查询(等主线程响应); - 同一时刻主线程的 Resized/Moved 回调阻塞在 cache.lock().unwrap()。 - A 持锁等主线程,主线程等锁 → 互等 6s → appfreeze。 上游已为 OHOS 跳过 is_minimized()/is_maximized() 同步查询(lib.rs:486-519), 但 cache.lock() 本身仍是阻塞锁,是当前死点。 修复:Moved(lib.rs:496)+ Resized(lib.rs:534)两个风暴路径,在 OHOS 上把 cache.lock() 换成 try_lock(),抢不到锁就跳过这一帧。close 时 save_window_state 会重新查询窗口当前尺寸落盘,跳帧不丢最终状态。其他平台保留原 lock()。 验证:重新构建部署后跑完整 autotest(261 项),全程 0 appfreeze(此前每次跑测 试都留 appfreeze)。faultlog 最近一次 appfreeze 时间戳停在修复前。
51428ae to
c683a18
Compare
- 7 plugin schema.json: add openHarmony platform enum (cli, opener, positioner, sql, store, upload, websocket). - notification reference.md: drop allow-permission-state from the default permission list (default.toml does not grant it); keep the notification:allow/deny-permission-state Permission Table rows since the permission_state command still exists (src/commands.rs) and the schema still defines them. - Cargo.lock: pin OHOS binding deps (napi-ohos/ohos-arkui-*/huawei-account). The window-state try_lock appfreeze fix is intentionally NOT included here; it is carried by PR Eulogizethesun#24 (the dedicated freeze fix with the accurate deadlock root-cause comment), per review Eulogizethesun#23.
- 7 plugin schema.json (cli, opener, positioner, sql, store, upload, websocket): add openHarmony platform enum. - Cargo.lock: pin OHOS binding deps (napi-ohos / ohos-arkui-* / huawei-account). Per review Eulogizethesun#23: - window-state try_lock appfreeze fix is NOT included here (carried by PR Eulogizethesun#24). - notification reference.md change removed from this PR entirely. The notification permission_state drift is pre-existing at base (permission_state exists in src/commands.rs but has no command .toml and is not granted by default.toml, while schema.json still lists it). It will be fixed consistently by a separate regeneration PR that drops permission_state from both schema.json and reference.md.
Eulogizethesun
left a comment
There was a problem hiding this comment.
根因存疑:AB-BA 死锁链条与 get_main_thread_env 的 thread_local 实现矛盾
修复实测 0 appfreeze 是经验事实,但 PR 给的根因解释和代码对不上,提出来一起复核。
PR 描述的死锁链
A 线程:
save_window_state持有cache.lock(),随后做同步 NAPI 查询(等主线程响应run_on_main_thread + recv);主线程:Resized/Moved 回调阻塞在cache.lock().unwrap()→ 互等 6s。
即:命令线程持锁 + 同步 NAPI 等主线程响应,主线程又等同一把锁 → 互等。
代码事实:命令线程的 NAPI getter 不会等主线程
save_window_state 命令是 #[command] pub async fn(cmd.rs:22),Tauri 标准行为是 spawn 到 tokio worker 线程,不是主线程。
而 OHOS 的同步 NAPI window getter(is_window_minimized / is_window_maximized 等,见 openharmony-ability window/mod.rs:475)走的是:
if let Some(env) = get_main_thread_env().borrow().as_ref() {
func.call(window_id) // 真正同步执行 ArkTS
} else {
crate::error!("Main thread env not available");
return Err(...) // 直接失败,不 marshal 到主线程
}get_main_thread_env() 读的是 thread_local MAIN_THREAD_ENV(openharmony-ability helper/mod.rs:29),注释明确:
/// Only returns Some when called from the main thread where set_main_thread_env was called.
所以:
| 调用线程 | get_main_thread_env() |
getter 行为 |
|---|---|---|
| 主线程(事件回调 / restore_state) | Some(env) |
真正 func.call → 同步执行 ArkTS |
| 非主线程(save_window_state 命令) | None |
Err(...) → tao 层 .unwrap_or_else(|e| false) → 返回默认值,不阻塞、不等主线程 |
→ 命令线程持锁期间没有"同步 NAPI 等主线程响应"这一步:getter 拿不到 env 直接 Err 返默认值,inner_size()/outer_position() 读的是 ArkTS 回调写入的本地镜像(self.app.content_rect() / window_rect(),tao ohos/mod.rs:1034/1048)也是非阻塞。命令线程持锁时间很短。
→ "A 持锁等主线程,主线程等锁 → 互等 6s" 这个环在当前实现下不成立。
(顺带:PR body 说机制是 run_on_main_thread + recv,inline 注释说是 get_main_thread_env + func.call, not run_on_main_thread —— 两处自相矛盾,inline 更接近代码,但即便按 inline 机制,非主线程同样不阻塞。)
那 try_lock 为什么实测有效?
静态分析解释不了。几种可能,需要排查:
- faultlog 符号化点是否真卡在
cache.lock。命令线程若不在此阻塞,那个符号化结论值得复核 —— 死点可能在别处(如主线程上update_state→ 真同步 NAPI getter 的重入,或WindowStateCache的std::sync::Mutex不可重入在被锁线程再次 lock 时的死锁)。 - 真实成因是主线程侧锁竞争/重入,try_lock 通过"抢不到就跳帧"恰好回避了表象 —— 属症状压制,非根因修复,可能在另一路径复现。
- 对 Tauri async 命令线程模型理解有偏差(低概率)。
建议
- 复核 resize appfreeze 时的 faultlog + 完整调用栈,确认持锁者与等锁者分别是谁、各自在哪条 NAPI 调用上。
- 确认 resize 期间是否真有并发
save_window_state命令执行;若 resize 期间未触发 save,则持锁者只剩主线程自己的事件回调,互等 6s 更难解释。 CloseRequested分支仍是cache.lock().unwrap()且持锁期间调update_state(该路径在主线程,NAPI getter 会真执行),与本 PR 修复的是同模式,建议一并评估是否需同样处理。
修复本身
try_lock 改动无害(跳帧不丢最终数据:落盘前 update_state 会用实时值刷新;平台隔离合规)—— 这部分没问题。只是根因解释对不上代码,先标记出来避免后续按错误机制做错误推断。
| if let Some(state) = c.get_mut(&label) { | ||
| state.width = size.width; | ||
| state.height = size.height; | ||
| // OHOS: use try_lock instead of a blocking lock. During a Resized |
There was a problem hiding this comment.
🟡 根因质疑 — 注释说 "another main-thread path may hold this lock and issue a synchronous NAPI call"。但 save_window_state 是 async command,跑在 tokio worker 线程而非主线程;worker 线程的 NAPI window getter 经 get_main_thread_env()(thread_local,非主线程返回 None)→ 直接 Err 返默认值,不会同步等主线程。详见总结评论里的完整分析。想确认:appfreeze 时持锁者到底是谁?
问题
examples/api装到鸿蒙 PC 上 resize 窗口时高频appfreeze(THREAD_BLOCK_6S),系统看门狗杀进程。2026-08-13 抓 faultlog +llvm-addr2line符号化确认死点。根因:AB-BA 死锁
save_window_state持有cache.lock(),随后做同步 NAPI 查询(等主线程响应run_on_main_thread+recv);cache.lock().unwrap()。A 持锁等主线程,主线程等锁 → 互等 6s → appfreeze。上游已为 OHOS 跳过
is_minimized()/is_maximized()同步查询(lib.rs:486-519),但cache.lock()本身仍是阻塞锁,是当前死点。修复
Moved(lib.rs:496)+Resized(lib.rs:534)两个风暴路径,在 OHOS 上把cache.lock()换成try_lock(),抢不到锁就跳过这一帧:save_window_state会重新查询窗口当前尺寸落盘,跳帧不影响最终状态。#[cfg(not(target_env = "ohos"))]保留原lock(),因为其他平台事件循环短路非阻塞。验证
重新构建部署后跑完整 autotest(261 项),全程 0 appfreeze(此前每次跑测试都留 appfreeze)。faultlog 里最近一次 appfreeze 时间戳停在修复前,修复后的多次 run 全无新增。
改动范围
仅
plugins/window-state/src/lib.rs(48 增 / 11 删)。不含 schema/Cargo.lock/notification 等无关改动(那些在另一个 commit,会分开提)。本 PR 只含一个文件