Skip to content

fix(ohos): window-state try_lock to prevent appfreeze on resize/move [urgent] - #24

Open
ddxwzc-boop wants to merge 1 commit into
Eulogizethesun:ohdevfrom
ddxwzc-boop:fix/ohos-window-state-freeze
Open

fix(ohos): window-state try_lock to prevent appfreeze on resize/move [urgent]#24
ddxwzc-boop wants to merge 1 commit into
Eulogizethesun:ohdevfrom
ddxwzc-boop:fix/ohos-window-state-freeze

Conversation

@ddxwzc-boop

Copy link
Copy Markdown

紧急同步 — OHOS PC resize 窗口时 appfreeze 导致应用频繁崩溃,已验证修复,请尽快 review/merge。

问题

examples/api 装到鸿蒙 PC 上 resize 窗口时高频 appfreeze(THREAD_BLOCK_6S),系统看门狗杀进程。2026-08-13 抓 faultlog + llvm-addr2line 符号化确认死点。

根因:AB-BA 死锁

窗口 resize → OHOS OnSizeChange → tao/tauri 事件循环
  → Window::on_window_event → window-state 插件 Resized 分支 (lib.rs:534)
  → cache.lock().unwrap()   ← 阻塞点
  • A 线程save_window_state 持有 cache.lock(),随后做同步 NAPI 查询(等主线程响应 run_on_main_thread + recv);
  • 主线程: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(),抢不到锁就跳过这一帧:

#[cfg(target_env = "ohos")]
{
    if let Ok(mut c) = cache.try_lock() {
        if let Some(state) = c.get_mut(&label) {
            state.width = size.width;
            state.height = size.height;
        }
    }
}
#[cfg(not(target_env = "ohos"))]
{ /* 原来的 lock().unwrap(),其他平台事件循环非阻塞 */ }
  • 不丢数据:close 时 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 只含一个文件

plugins/window-state/src/lib.rs | 59 +++++++++++++++++++++++++++++++++--------
 1 file changed, 48 insertions(+), 11 deletions(-)

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 时间戳停在修复前。
@ddxwzc-boop
ddxwzc-boop force-pushed the fix/ohos-window-state-freeze branch from 51428ae to c683a18 Compare August 17, 2026 02:41
ddxwzc-boop added a commit to ddxwzc-boop/plugins-workspace that referenced this pull request Aug 17, 2026
- 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.
ddxwzc-boop added a commit to ddxwzc-boop/plugins-workspace that referenced this pull request Aug 17, 2026
- 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 Eulogizethesun left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

根因存疑: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 为什么实测有效?

静态分析解释不了。几种可能,需要排查:

  1. faultlog 符号化点是否真卡在 cache.lock。命令线程若不在此阻塞,那个符号化结论值得复核 —— 死点可能在别处(如主线程上 update_state → 真同步 NAPI getter 的重入,或 WindowStateCachestd::sync::Mutex 不可重入在被锁线程再次 lock 时的死锁)。
  2. 真实成因是主线程侧锁竞争/重入,try_lock 通过"抢不到就跳帧"恰好回避了表象 —— 属症状压制,非根因修复,可能在另一路径复现。
  3. 对 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

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 根因质疑 — 注释说 "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 时持锁者到底是谁?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants