docs: clarify binding refresh and worker publication boundaries
This commit is contained in:
+13
-1
@@ -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()
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -149,6 +149,8 @@ pub fn connect(
|
||||
}
|
||||
|
||||
/// 在只读调用中重算失效绑定,采集新的读取版本,再将成功结果应用到目标属性。
|
||||
/// UiBridge::refresh_bindings 在每轮展示刷新开始时调用;compute 只负责返回值,不能直接写已有模型。
|
||||
/// 本函数退出只读调用后才验证并写目标属性,这些宿主写入可以使其他绑定失效,因而按完整轮次求固定点。
|
||||
pub fn refresh(
|
||||
vm: &mut Vm,
|
||||
nodes: &[WeakObjectRef],
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user