From 0cd708f8c7d0cf79b67240b4ce01e4ddef4ea6c7 Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Thu, 12 Nov 2026 19:57:03 +0800 Subject: [PATCH] docs: clarify binding refresh and worker publication boundaries --- src/bridge/ui_bridge.rs | 14 +++++++++++++- src/bridge/ui_bridge/calls.rs | 3 +++ src/model/binding.rs | 2 ++ src/runtime/api/worker.rs | 15 +++++++++++++++ src/runtime/endor_host.rs | 4 ++-- 5 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/bridge/ui_bridge.rs b/src/bridge/ui_bridge.rs index 8b89459..bf3efac 100644 --- a/src/bridge/ui_bridge.rs +++ b/src/bridge/ui_bridge.rs @@ -7,6 +7,13 @@ //! Function/TypeMethod/NativeFactory 是语言调用适配,Session 保存跨调用的窗口展示状态; //! 真正的排版/绘制交给 render,业务对象与 Mount 数据交给 model,均不在这里操作 OS 窗口。 //! 本模块会在 worker 中访问 VM;Arc/Mutex 只用于共享原生会话,不授权 GUI 线程直接调用 VM。 +//! +//! 阅读调用链时应分清三个入口:bootstrap::install 创建类型和原生 callable; +//! calls 中实现的 Function::invoke 接收源程序的工厂/方法调用;endor_host 的 step_refresh 则推进展示刷新。 +//! 刷新先计算绑定,再用 prepare_frame 判断是否有工作,begin/resume_layout_frame 求几何, +//! begin/resume_paint_frame 求绘制命令,最终由 endor_host 汇总稳定的逐窗文档。 +//! commit_frame 是桥接内部的 Mount/几何提交,生命周期回调仍可令依赖失效; +//! 它不是向 GUI 发布成功的标志,也不意味着后来失败的整段源输入会回滚先前业务写入。 mod bootstrap; mod calls; mod developer_pages; @@ -1913,7 +1920,7 @@ impl UiBridge { } Ok(()) } - /// endor_host 在刷新稳定循环中调用,重算失效绑定;回调可继续改变对象,随后 prepare_frame 才排空变化。 + /// endor_host 在刷新稳定循环中调用,先只读计算失效绑定,再由宿主应用目标属性;随后 prepare_frame 才排空这些变化。 pub fn refresh_bindings(&self, vm: &mut Vm) -> Result<()> { // 只在锁内快照弱节点与宿主桌面集合,实际绑定回调在锁外执行,避免回调访问 UI 时死锁。 let (nodes, desktops) = { @@ -2211,6 +2218,7 @@ impl UiBridge { }; // 准备私有布局草稿与缓存副本,之后的源调用在锁外运行;正式状态等完成时再写回。 let (mut layout, cache, desktop, page, nodes) = { + // 克隆布局/缓存作为本轮私有求解输入,page 同时保留显式清空请求;此处尚不消费 pending 或覆盖 active。 let state = self .session .lock() @@ -2237,6 +2245,7 @@ impl UiBridge { let surface = root.surface.clone().unwrap_or_default(); layout.page_mode = root.surface.as_ref().map(|surface| surface.mode); { + // 只借用当前全局出现身份下界,放锁后才在私有 layout 上创建结构;即使取消也不允许复用已暴露 ID。 let state = self .session .lock() @@ -2302,6 +2311,7 @@ impl UiBridge { /// endor_host 在布局/绘制挂起让出 worker 时调用,累计诊断次数,不改变下一片执行预算。 pub fn note_refresh_yield(&self) -> Result<()> { + // 本锁仅保护跨窗口共享的诊断累计值,不恢复续体、不选择窗口,也不改变本轮是否需要发布。 let mut state = self .session .lock() @@ -2317,6 +2327,7 @@ impl UiBridge { cancelled_layout: bool, cancelled_paint: bool, ) -> Result<()> { + // 三个统计在同一短锁内更新,让 stats 不会看到“重启已记账而对应取消尚未记账”的中间值。 let mut state = self .session .lock() @@ -2466,6 +2477,7 @@ impl UiBridge { let result: Result = (|| { self.select_window(window)?; { + // select_window 已恢复 context.window,锁只覆盖读取该窗 Mount 数据和原生投影,源生命周期钩子稍后在锁外执行。 let state = self .session .lock() diff --git a/src/bridge/ui_bridge/calls.rs b/src/bridge/ui_bridge/calls.rs index 15c80a1..0a1a073 100644 --- a/src/bridge/ui_bridge/calls.rs +++ b/src/bridge/ui_bridge/calls.rs @@ -288,8 +288,10 @@ impl NativeCallable for Function { // 页面是 Block 的子集,同一页跨窗口迁移不会增加业务页面计数。 let mut pages = 0_i64; for weak in &session.nodes { + // 弱索引可能已死亡;只统计成功升级的对象,不能用 nodes.len() 代表当前存活 Block 数。 if let Some(object) = cx.upgrade(*weak)? { blocks += 1; + // Page 是这些存活 Block 的子集,按 payload 标志计一次,不按标签数或 Mount 数重复统计。 if cx.store().payload::(object, "Block")?.page { pages += 1; } @@ -787,6 +789,7 @@ impl NativeCallable for Function { // 平台索引转换和容器长度检查分开,len 本身允许表示“插到末尾”。 let position = usize::try_from(integer(args.first())?) .map_err(|_| invalid("child position out of range"))?; + // 插入位置是孩子之间的缝,允许等于 len 表示末尾;大于 len 则在分配 slot 和修改子序列之前拒绝。 if position > data.children.len() { return Err(invalid("child position out of range")); } diff --git a/src/model/binding.rs b/src/model/binding.rs index 6a78b87..8a94ab8 100644 --- a/src/model/binding.rs +++ b/src/model/binding.rs @@ -149,6 +149,8 @@ pub fn connect( } /// 在只读调用中重算失效绑定,采集新的读取版本,再将成功结果应用到目标属性。 +/// UiBridge::refresh_bindings 在每轮展示刷新开始时调用;compute 只负责返回值,不能直接写已有模型。 +/// 本函数退出只读调用后才验证并写目标属性,这些宿主写入可以使其他绑定失效,因而按完整轮次求固定点。 pub fn refresh( vm: &mut Vm, nodes: &[WeakObjectRef], diff --git a/src/runtime/api/worker.rs b/src/runtime/api/worker.rs index de404b0..6ab43a4 100644 --- a/src/runtime/api/worker.rs +++ b/src/runtime/api/worker.rs @@ -164,6 +164,7 @@ impl Worker { let Ok(mut current) = namespace.write() else { return error_response(StatusCode(500), "namespace lock poisoned"); }; + // 锁外退役期间可能接受更新提交;过期请求不能再删除当前世代文档中的同编号窗口。 if current.submission != epoch { return superseded(epoch); } @@ -186,12 +187,14 @@ impl Worker { document.frame.session = 0; document.frame.focus = None; for view in document.windows.values_mut() { + // 丢弃的是整个共享 VM,所以剩余每个副窗都撤销交互身份,不能只清主画面或刚关闭的那个窗口。 view.frame.session = 0; view.frame.focus = None; } current.runtime_session = 0; current.window_commands.clear(); } + // 发布退役后的完整文档;只有 Some 才继续移除视口记录并唤醒 GUI,过期或校验失败都提前返回。 match current.commit(epoch, document) { Ok(Some(_)) => {} Ok(None) => return superseded(epoch), @@ -283,6 +286,7 @@ impl Worker { // success 可仅有命令而无新帧;错误发布故障条,panic 还要丢弃 VM 与命令世代。 let (document, commands) = match result { Ok(Ok((document, commands))) => { + // 背景轮询成功有两类结果:有新文档则走下面原子提交,无文档则仅发布可能存在的窗口命令。 if let Some(document) = document { (document, Some(commands)) } else { @@ -290,6 +294,7 @@ impl Worker { let projected = !commands.is_empty(); current.publish_window_commands(id, commands); drop(current); + // 已释放 Namespace 写锁后才唤醒 GUI;没有命令也没有新帧时保持空闲,不凭轮询次数触发绘制。 if projected { notify(); } @@ -320,10 +325,12 @@ impl Worker { }; // 只有提交成功才发布同世代原生命令和通知 GUI,错误/过期结果不能算一次有效刷新。 let changed = matches!(current.commit(epoch, document), Ok(Some(_))); + // 有效文档和对应命令属于同一会话,提交未接受时不能单独交付命令造成画面/原生窗口状态错配。 if changed && let Some(commands) = commands { current.publish_window_commands(id, commands); } drop(current); + // changed 来自权威 commit 结果,不把失败或被替代的候选当作可见更新通知。 if changed { notify(); } @@ -699,6 +706,7 @@ impl Worker { // 是否需对可见帧加故障条取决于 VM 的实际执行状态,普通编译失败不等于整个域已损坏。 let faulted = session.is_faulted(); if session.has_pending_refresh() { + // 只有正常完成业务输入的结果可保存在 PendingInput;恢复时沿用 result,不再执行一次原始输入。 if let Ok(Ok((result, _))) = result { return RequestProgress::Pending(PendingRequest::Input(Box::new(PendingInput { epoch, @@ -777,6 +785,7 @@ impl Worker { "input runtime was discarded", )); }; + // 接受序号与真实 VM 会话号都须匹配;同 epoch 中若换过会话,也不能把旧 PendingInput 接到新堆续跑。 if *epoch != input.epoch || session.id != input.id { return RequestProgress::Complete(superseded(job.submission)); } @@ -858,6 +867,7 @@ impl Worker { // rendered 只表示此次交付带新文档,成功执行但页面未变仍可返回 value/session;不因此虚增 revision。 let rendered = result.document.is_some(); if let Some(document) = result.document { + // 可见文档必须先完整验证并被当前 epoch 接受,再发布其窗口命令;不能先回成功响应后补提交。 match current.commit(epoch, document) { Ok(Some(_)) => {} Ok(None) => return superseded(job.submission), @@ -868,6 +878,7 @@ impl Worker { // 在释放锁前读取这次确定的 revision,响应不再为字符串化借用共享状态。 let revision = current.revision; drop(current); + // 有新画面或仅有原生命令都要通知;纯值计算没有两者时不强行重绘,响应仍正常返回值。 if rendered || projected { notify(); } @@ -887,6 +898,7 @@ impl Worker { .unwrap_or_else(|| format!("{error:#}").chars().take(1024).collect()); document.frame.fault = Some(fault.clone()); for view in document.windows.values_mut() { + // 这些副窗共享已故障的持久 VM,同步显示同一原因,不能只在发生事件的一个窗口标记故障。 view.frame.fault = Some(fault.clone()); } // 尽力提交故障条,主响应仍保留原始执行错误;不能因二次故障覆盖真正原因。 @@ -902,6 +914,7 @@ impl Worker { document.frame.fault = Some("Runtime discarded after an internal execution failure".into()); for view in document.windows.values_mut() { + // panic 后保留最后完整画面并附统一故障条,下面会撤销运行时资格,不能继续向旧对象派发输入。 view.frame.fault = document.frame.fault.clone(); } // 输入 panic 保留最近画面但撤销运行时/命令并发布故障条,再将 self.repl 丢弃。 @@ -1113,6 +1126,7 @@ fn build( Err(_) => error_response(StatusCode(500), "expression execution failed internally"), }; } + // 一次性构建开始前先拒绝过期 job,节省无权发布的执行;锁外求值结束后仍须再次核验,不能只检查一次。 match namespace.read() { Ok(current) if current.submission != job.submission => return superseded(job.submission), Err(_) => return error_response(StatusCode(500), "namespace lock is poisoned"), @@ -1127,6 +1141,7 @@ fn build( let Ok(mut current) = namespace.write() else { return error_response(StatusCode(500), "namespace lock is poisoned"); }; + // 这是计算后的第二次检查;期间新提交即使源码相同也拥有新 submission,旧候选不得覆盖它。 if current.submission != job.submission { return superseded(job.submission); } diff --git a/src/runtime/endor_host.rs b/src/runtime/endor_host.rs index 0fd2cd7..0561f05 100644 --- a/src/runtime/endor_host.rs +++ b/src/runtime/endor_host.rs @@ -846,9 +846,9 @@ impl ReplSession { /// poll_refresh 每量子调用一次,推进稳定循环、一个窗口或一片布局/绘制;true 只表示全轮无新增变化。 fn step_refresh(&mut self, work: &mut RefreshWork) -> Result { // 阶段一:新一轮先刷新绑定,再取得稳定窗口遍历顺序。 - // 每轮先使属性绑定达到固定点,再取窗口顺序;生命周期或绑定可能改变窗口/页面结构,因此需要多轮确认。 + // 每轮先使属性绑定达到固定点,再取窗口顺序;绑定结果应用与生命周期写入可能令依赖再次失效,因此需要多轮确认。 if work.pass_start { - // 绑定/生命周期可能改窗口登记,新的稳定轮必须重新刷新绑定并获取窗口顺序。 + // 生命周期可能改窗口登记,绑定结果可能改视觉属性;新的稳定轮必须重新刷新绑定并获取窗口顺序。 self.bridge.refresh_bindings(&mut self.candidate.vm)?; work.windows = self.bridge.window_ids()?.into(); work.current = None;