feat: resume source paint across worker turns and cancel unpublished commands
This commit is contained in:
+56
-16
@@ -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
@@ -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
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user