feat: resume source paint across worker turns and cancel unpublished commands

This commit is contained in:
2026-10-23 11:07:49 +08:00
parent 180a08ea63
commit acaef9ae29
3 changed files with 202 additions and 47 deletions
+56 -16
View File
@@ -200,12 +200,19 @@ fn compile_plan_with_globals(
/// One real VM, owned only by the existing execution worker. No history replay.
/// The UI bridge shares the same managed object heap and publishes atomic frames.
struct PendingPaint {
window: u64,
document: RenderDocument,
work: Box<crate::source_paint::Work>,
}
struct RefreshWork {
documents: std::collections::BTreeMap<u64, RenderDocument>,
windows: std::collections::VecDeque<u64>,
current: Option<u64>,
root: Option<std::sync::Arc<crate::ui_bridge::UiNode>>,
pending: Option<Box<crate::ui_bridge::PendingLayout>>,
paint: Option<Box<PendingPaint>>,
pass_start: bool,
pass_changed: bool,
changed: bool,
@@ -595,6 +602,7 @@ impl ReplSession {
current: None,
root: None,
pending: None,
paint: None,
pass_start: true,
pass_changed: false,
changed: false,
@@ -625,6 +633,9 @@ impl ReplSession {
}
Err(error) => {
self.faulted = true;
if let Some(paint) = work.paint.take() {
self.bridge.cancel_paint_frame(&mut self.candidate.vm, paint.window, *paint.work)?;
}
if let Some(pending) = work.pending.take() {
self.bridge
.abort_layout_frame(&mut self.candidate.vm, *pending)?;
@@ -656,6 +667,11 @@ impl ReplSession {
return Ok(true);
};
self.bridge.select_window(id)?;
if let Some(paint) = work.paint.take() {
let PendingPaint { window, document, work: paint_work } = *paint;
let progress = self.bridge.resume_paint_frame(&mut self.candidate.vm, *paint_work)?;
return Self::accept_window_paint(work, window, document, progress);
}
let progress = if let Some(pending) = work.pending.take() {
self.bridge
.resume_layout_frame(&mut self.candidate.vm, *pending)?
@@ -694,15 +710,32 @@ impl ReplSession {
if work.work > 200_000 {
bail!("layout dependencies did not stabilize within the rendering work budget");
}
self.bridge.paint_frame(
let progress = self.bridge.begin_paint_frame(
&mut self.candidate.vm,
document.ui_root.as_deref(),
&mut document.frame,
document.ui_root.clone(),
std::mem::take(&mut document.frame),
)?;
document.validate()?;
work.documents.insert(id, document);
work.pass_changed = true;
work.changed = true;
Self::accept_window_paint(work, id, document, progress)
}
fn accept_window_paint(
work: &mut RefreshWork,
id: u64,
mut document: RenderDocument,
progress: crate::source_paint::Progress,
) -> Result<bool> {
match progress {
crate::source_paint::Progress::Pending(paint) => {
work.paint = Some(Box::new(PendingPaint { window: id, document, work: paint }));
}
crate::source_paint::Progress::Complete(draft) => {
document.frame = draft.frame;
document.validate()?;
work.documents.insert(id, document);
work.pass_changed = true;
work.changed = true;
}
}
Ok(false)
}
@@ -759,6 +792,13 @@ impl ReplSession {
return Ok(());
};
let cancelled = work.pending.is_some();
let cancelled_paint = work.paint.is_some();
if let Some(paint) = work.paint.take()
&& let Err(error) = self.bridge.cancel_paint_frame(&mut self.candidate.vm, paint.window, *paint.work)
{
self.faulted = true;
return Err(error);
}
if let Some(pending) = work.pending.take()
&& let Err(error) = self
.bridge
@@ -767,7 +807,7 @@ impl ReplSession {
self.faulted = true;
return Err(error);
}
if let Err(error) = self.bridge.note_refresh_restart(cancelled) {
if let Err(error) = self.bridge.note_refresh_restart(cancelled, cancelled_paint) {
self.faulted = true;
return Err(error);
}
@@ -1023,14 +1063,14 @@ impl ReplSession {
impl Drop for ReplSession {
fn drop(&mut self) {
if let Some(mut work) = self.refresh.take()
&& let Some(pending) = work.pending.take()
{
// Close the read-only continuation and checkpoint roots before
// Candidate releases the globals/services of a discarded domain.
let _ = self
.bridge
.cancel_layout_frame(&mut self.candidate.vm, *pending);
if let Some(mut work) = self.refresh.take() {
if let Some(paint) = work.paint.take() {
let _ = self.bridge.cancel_paint_frame(&mut self.candidate.vm, paint.window, *paint.work);
}
if let Some(pending) = work.pending.take() {
// Close the read-only call/checkpoint before Candidate roots.
let _ = self.bridge.cancel_layout_frame(&mut self.candidate.vm, *pending);
}
}
}
}
+92 -16
View File
@@ -271,13 +271,77 @@ impl NativeCallable for Prepare {
}
}
pub fn render(
pub struct Draft {
pub frame: Frame,
pub reads: BTreeSet<ObjectRef>,
pub slices: usize,
}
#[must_use = "pending paint must be resumed or cancelled on its owning VM"]
pub enum Progress {
Pending(Box<Work>),
Complete(Box<Draft>),
}
/// Only unpublished native commands and an owning prepare root cross a pause.
pub struct Work {
desktop: ObjectRef,
prepare: ObjectRef,
output: Output,
root: Option<Arc<UiNode>>,
frame: Frame,
slices: usize,
}
impl Work {
pub fn resume(mut self, vm: &mut Vm, slice: usize) -> Result<Progress> {
self.slices += 1;
let progress = vm.resume_read_only_call(slice);
self.accept(vm, progress)
}
pub fn cancel(self, vm: &mut Vm) -> Result<()> {
let cancelled = vm.cancel_read_only_call();
let released = vm.release(self.prepare);
cancelled?;
released?;
Ok(())
}
fn accept(
self,
vm: &mut Vm,
progress: Result<endor_core::ExecutionProgress, VmError>,
) -> Result<Progress> {
match progress {
Ok(endor_core::ExecutionProgress::Pending { .. }) => Ok(Progress::Pending(Box::new(self))),
Ok(endor_core::ExecutionProgress::Complete { value, reads, .. }) => {
vm.release(self.prepare)?;
vm.release_value(value)?;
finish(vm, self, reads)
}
Err(error) => {
let diagnostic = crate::endor_host::runtime_error(vm, error);
self.cancel(vm)?;
Err(diagnostic)
}
}
}
}
pub fn start(
vm: &mut Vm,
desktop: ObjectRef,
mounts: Vec<(u64, WeakObjectRef)>,
root: Option<&UiNode>,
frame: &mut Frame,
) -> Result<BTreeSet<ObjectRef>> {
root: Option<Arc<UiNode>>,
frame: Frame,
slice: usize,
) -> Result<Progress> {
anyhow::ensure!(slice > 0, "paint slice must be positive");
anyhow::ensure!(
!vm.has_suspended_call() && !vm.object_store().is_read_only(),
"cannot start paint while another read-only execution is active"
);
let output = Arc::new(Mutex::new(PaintOutput::default()));
let prepare = vm.allocate(ContextNativeFunction::new(
"prepare source paint",
@@ -288,23 +352,30 @@ pub fn render(
},
vec![desktop],
))?;
let result = vm.call_read_only_with_limits(
let work = Work { desktop, prepare, output, root, frame, slices: 1 };
let result = vm.start_read_only_call(
prepare,
&[],
endor_core::ExecutionLimits {
max_frames: 128,
max_instructions: 1_000_000,
},
endor_core::ExecutionLimits { max_frames: 128, max_instructions: 1_000_000 },
slice,
);
vm.release(prepare)?;
let (value, reads) = result.map_err(|error| crate::endor_host::runtime_error(vm, error))?;
vm.release_value(value)?;
let output = output
work.accept(vm, result)
}
fn finish(vm: &mut Vm, mut work: Work, reads: Vec<ObjectRef>) -> Result<Progress> {
let output = work.output
.lock()
.map_err(|_| anyhow::anyhow!("paint output poisoned"))?;
if output.commands.is_empty() {
return Ok(BTreeSet::new());
return Ok(Progress::Complete(Box::new(Draft {
frame: work.frame,
reads: BTreeSet::new(),
slices: work.slices,
})));
}
let root = work.root.as_deref();
let desktop = work.desktop;
let frame = &mut work.frame;
let mut nodes = HashMap::new();
let mut pending: Vec<_> = root.into_iter().collect();
while let Some(node) = pending.pop() {
@@ -341,7 +412,7 @@ pub fn render(
frame.commands = commands;
frame.command_clips = clips;
let registry = object(vm.get_attr(desktop, "types")?)?;
Ok(reads
let reads = reads
.into_iter()
.filter(|object| {
*object != desktop
@@ -350,5 +421,10 @@ pub fn render(
.object(*object)
.is_ok_and(|object| !object.payload().as_any().is::<Mount>())
})
.collect())
.collect();
Ok(Progress::Complete(Box::new(Draft {
frame: work.frame,
reads,
slices: work.slices,
})))
}
+54 -15
View File
@@ -231,6 +231,8 @@ struct Session {
refresh_yields: usize,
refresh_restarts: usize,
layout_cancels: usize,
paint_cancels: usize,
paint_slices: usize,
}
impl std::ops::Deref for Session {
@@ -1159,6 +1161,8 @@ impl NativeCallable for Function {
("refreshYields", session.refresh_yields as i64),
("refreshRestarts", session.refresh_restarts as i64),
("layoutCancels", session.layout_cancels as i64),
("paintCancels", session.paint_cancels as i64),
("paintSlices", session.paint_slices as i64),
] {
cx.set_attr(stats, name, Value::Number(value.into()))?;
}
@@ -2451,12 +2455,12 @@ impl UiBridge {
result
}
pub fn paint_frame(
pub fn begin_paint_frame(
&self,
vm: &mut Vm,
root: Option<&UiNode>,
frame: &mut crate::document::Frame,
) -> Result<()> {
root: Option<Arc<UiNode>>,
frame: crate::document::Frame,
) -> Result<crate::source_paint::Progress> {
let (desktop, mounts) = {
let state = self
.session
@@ -2477,16 +2481,48 @@ impl UiBridge {
.collect::<Result<Vec<_>>>()?;
(state.desktop, mounts)
};
let reads = crate::source_paint::render(vm, desktop, mounts, root, frame)?;
let reads = reads
.into_iter()
.map(|object| vm.change_version(object).map(|version| (object, version)))
.collect::<Result<std::collections::BTreeMap<_, _>, _>>()?;
self.session
.lock()
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?
.paint_reads = reads;
Ok(())
let progress = crate::source_paint::start(vm, desktop, mounts, root, frame, 256)?;
self.accept_paint_progress(vm, progress)
}
pub fn resume_paint_frame(
&self,
vm: &mut Vm,
work: crate::source_paint::Work,
) -> Result<crate::source_paint::Progress> {
let progress = work.resume(vm, 256)?;
self.accept_paint_progress(vm, progress)
}
fn accept_paint_progress(
&self,
vm: &Vm,
progress: crate::source_paint::Progress,
) -> Result<crate::source_paint::Progress> {
if let crate::source_paint::Progress::Complete(draft) = &progress {
let reads = draft.reads.iter().copied()
.map(|object| vm.change_version(object).map(|version| (object, version)))
.collect::<Result<std::collections::BTreeMap<_, _>, _>>()?;
let mut state = self.session.lock().map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
state.paint_reads = reads;
state.paint_slices = draft.slices;
}
Ok(progress)
}
pub fn cancel_paint_frame(
&self,
vm: &mut Vm,
window: u64,
work: crate::source_paint::Work,
) -> Result<()> {
let cancelled = work.cancel(vm);
let invalidated = self.select_window(window).and_then(|()| {
self.session.lock().map_err(|_| anyhow::anyhow!("UI session poisoned"))?.force_frame = true;
Ok(())
});
cancelled?;
invalidated
}
pub fn begin_layout_frame(
@@ -2615,7 +2651,7 @@ impl UiBridge {
Ok(())
}
pub fn note_refresh_restart(&self, cancelled_layout: bool) -> Result<()> {
pub fn note_refresh_restart(&self, cancelled_layout: bool, cancelled_paint: bool) -> Result<()> {
let mut state = self
.session
.lock()
@@ -2624,6 +2660,7 @@ impl UiBridge {
state.layout_cancels = state
.layout_cancels
.saturating_add(usize::from(cancelled_layout));
state.paint_cancels = state.paint_cancels.saturating_add(usize::from(cancelled_paint));
Ok(())
}
@@ -2827,6 +2864,8 @@ pub fn install(vm: &mut Vm, roots: &mut Vec<ObjectRef>) -> Result<UiBridge> {
refresh_yields: 0,
refresh_restarts: 0,
layout_cancels: 0,
paint_cancels: 0,
paint_slices: 0,
}));
let mut public_functions = Vec::new();
let mut factories = Vec::new();