docs: clarify binding refresh and worker publication boundaries

This commit is contained in:
2026-11-12 19:57:03 +08:00
parent 7f7464ae2b
commit 0cd708f8c7
5 changed files with 35 additions and 3 deletions
+13 -1
View File
@@ -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<crate::document::Frame> = (|| {
self.select_window(window)?;
{
// select_window 已恢复 context.window,锁只覆盖读取该窗 Mount 数据和原生投影,源生命周期钩子稍后在锁外执行。
let state = self
.session
.lock()
+3
View File
@@ -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::<block::Block>(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"));
}
+2
View File
@@ -149,6 +149,8 @@ pub fn connect(
}
/// 在只读调用中重算失效绑定,采集新的读取版本,再将成功结果应用到目标属性。
/// UiBridge::refresh_bindings 在每轮展示刷新开始时调用;compute 只负责返回值,不能直接写已有模型。
/// 本函数退出只读调用后才验证并写目标属性,这些宿主写入可以使其他绑定失效,因而按完整轮次求固定点。
pub fn refresh(
vm: &mut Vm,
nodes: &[WeakObjectRef],
+15
View File
@@ -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);
}
+2 -2
View File
@@ -846,9 +846,9 @@ impl ReplSession {
/// poll_refresh 每量子调用一次,推进稳定循环、一个窗口或一片布局/绘制;true 只表示全轮无新增变化。
fn step_refresh(&mut self, work: &mut RefreshWork) -> Result<bool> {
// 阶段一:新一轮先刷新绑定,再取得稳定窗口遍历顺序。
// 每轮先使属性绑定达到固定点,再取窗口顺序;生命周期或绑定可能改变窗口/页面结构,因此需要多轮确认。
// 每轮先使属性绑定达到固定点,再取窗口顺序;绑定结果应用与生命周期写入可能令依赖再次失效,因此需要多轮确认。
if work.pass_start {
// 绑定/生命周期可能改窗口登记,新的稳定轮必须重新刷新绑定并获取窗口顺序。
// 生命周期可能改窗口登记,绑定结果可能改视觉属性;新的稳定轮必须重新刷新绑定并获取窗口顺序。
self.bridge.refresh_bindings(&mut self.candidate.vm)?;
work.windows = self.bridge.window_ids()?.into();
work.current = None;