refactor: retain refresh traversal and unpublished window documents across polls
This commit is contained in:
@@ -506,3 +506,6 @@ source_layout::start now returns either Complete(Draft) or Pending(Work). The ho
|
||||
|
||||
|
||||
UiBridge::begin_layout_frame/resume_layout_frame now expose PendingLayout around the source job. This bridge-level continuation also owns its target window, page snapshot, tab projection, viewport restoration and retained lifecycle hook root. Only the Complete branch projects scroll/focus, captures dependency versions, merges derived caches, commits Mount geometry and runs deferred hooks. Resume restores the saved window context, and failure releases hook roots. layout_frame remains a synchronous adapter over this protocol. Cross-worker-turn scheduling and cancellation/rollback of prepared mounts are still unfinished; callers must not drop a pending bridge layout as a substitute for cancellation.
|
||||
|
||||
|
||||
ReplSession now owns a resumable RefreshWork containing window traversal, pending bridge layout, unpublished per-window documents and the cumulative refresh work budget. poll_refresh advances one refresh stage or VM quantum; only a complete stable pass replaces the session's full document map. Synchronous callers drain the same state machine. Failure stops the domain, releases any pending bridge work and preserves the externally published picture. refreshYields counts cooperative refresh returns, not wall-clock time. Prepared-Mount live rollback remains unfinished.
|
||||
|
||||
@@ -158,3 +158,6 @@ source_layout::start now returns either Complete(Draft) or Pending(Work). The ho
|
||||
|
||||
|
||||
UiBridge::begin_layout_frame/resume_layout_frame now expose PendingLayout around the source job. This bridge-level continuation also owns its target window, page snapshot, tab projection, viewport restoration and retained lifecycle hook root. Only the Complete branch projects scroll/focus, captures dependency versions, merges derived caches, commits Mount geometry and runs deferred hooks. Resume restores the saved window context, and failure releases hook roots. layout_frame remains a synchronous adapter over this protocol. Cross-worker-turn scheduling and cancellation/rollback of prepared mounts are still unfinished; callers must not drop a pending bridge layout as a substitute for cancellation.
|
||||
|
||||
|
||||
ReplSession now owns a resumable RefreshWork containing window traversal, pending bridge layout, unpublished per-window documents and the cumulative refresh work budget. poll_refresh advances one refresh stage or VM quantum; only a complete stable pass replaces the session's full document map. Synchronous callers drain the same state machine. Failure stops the domain, releases any pending bridge work and preserves the externally published picture. refreshYields counts cooperative refresh returns, not wall-clock time. Prepared-Mount live rollback remains unfinished.
|
||||
|
||||
+227
-101
@@ -200,6 +200,23 @@ 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 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>>,
|
||||
pass_start: bool,
|
||||
pass_changed: bool,
|
||||
changed: bool,
|
||||
work: usize,
|
||||
}
|
||||
|
||||
enum RefreshProgress {
|
||||
Pending,
|
||||
Complete(Option<RenderDocument>),
|
||||
}
|
||||
|
||||
pub struct ReplSession {
|
||||
candidate: Candidate,
|
||||
bridge: ui_bridge::UiBridge,
|
||||
@@ -211,6 +228,7 @@ pub struct ReplSession {
|
||||
faulted: bool,
|
||||
program_root: Option<CodeNode>,
|
||||
documents: std::collections::BTreeMap<u64, RenderDocument>,
|
||||
refresh: Option<RefreshWork>,
|
||||
}
|
||||
pub struct ReplResult {
|
||||
pub value: String,
|
||||
@@ -333,6 +351,7 @@ impl ReplSession {
|
||||
faulted: false,
|
||||
program_root: None,
|
||||
documents: std::collections::BTreeMap::new(),
|
||||
refresh: None,
|
||||
})
|
||||
}
|
||||
pub fn execute(&mut self, source: &str, namespace: &str) -> Result<ReplResult> {
|
||||
@@ -471,116 +490,200 @@ impl ReplSession {
|
||||
result
|
||||
}
|
||||
|
||||
pub fn has_pending_refresh(&self) -> bool {
|
||||
self.refresh.is_some()
|
||||
}
|
||||
|
||||
fn refresh_document(&mut self) -> Result<Option<RenderDocument>> {
|
||||
let mut work = 0;
|
||||
let mut changed = false;
|
||||
let mut mutated_mounts = false;
|
||||
let result = (|| {
|
||||
loop {
|
||||
if let Err(error) = self.bridge.refresh_bindings(&mut self.candidate.vm) {
|
||||
self.faulted = true;
|
||||
return Err(error);
|
||||
}
|
||||
let mut pass_changed = false;
|
||||
for id in self.bridge.window_ids()? {
|
||||
self.bridge.select_window(id)?;
|
||||
while let Some(root) = self.bridge.prepare_frame(&mut self.candidate.vm)? {
|
||||
mutated_mounts = true;
|
||||
let frame = self
|
||||
.bridge
|
||||
.layout_frame(&mut self.candidate.vm, root.clone())?;
|
||||
let mut document = RenderDocument {
|
||||
windows: std::collections::BTreeMap::new(),
|
||||
source: self.source.clone(),
|
||||
root: Some(self.program_root.clone().unwrap_or_else(|| CodeNode {
|
||||
id: format!("console-session-{}", self.id),
|
||||
name: format!("Console session {}", self.id),
|
||||
kind: "repl-session".into(),
|
||||
start_line: 1,
|
||||
end_line: self.source.lines().count(),
|
||||
children: self.nodes.clone(),
|
||||
})),
|
||||
frame,
|
||||
ui_root: root.clone(),
|
||||
};
|
||||
document.frame.session = self.id;
|
||||
document.validate()?;
|
||||
work += document.frame.mounts.len().max(1);
|
||||
if work > 200_000 {
|
||||
bail!(
|
||||
"layout dependencies did not stabilize within the rendering work budget"
|
||||
);
|
||||
}
|
||||
self.bridge.paint_frame(
|
||||
&mut self.candidate.vm,
|
||||
root.as_deref(),
|
||||
&mut document.frame,
|
||||
)?;
|
||||
document.validate()?;
|
||||
self.documents.insert(id, document);
|
||||
pass_changed = true;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if !pass_changed {
|
||||
break;
|
||||
}
|
||||
loop {
|
||||
match self.poll_refresh(false)? {
|
||||
RefreshProgress::Pending => std::thread::yield_now(),
|
||||
RefreshProgress::Complete(document) => return Ok(document),
|
||||
}
|
||||
self.bridge.finish_closed(&mut self.candidate.vm)?;
|
||||
let ids = self.bridge.window_ids()?;
|
||||
self.documents.retain(|id, _| ids.contains(id));
|
||||
if !changed {
|
||||
return Ok(None);
|
||||
}
|
||||
let mut main = self
|
||||
.documents
|
||||
.get(&crate::window::MAIN_WINDOW)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| RenderDocument {
|
||||
source: self.source.clone(),
|
||||
frame: crate::document::Frame {
|
||||
session: self.id,
|
||||
..crate::document::Frame::default()
|
||||
},
|
||||
..RenderDocument::default()
|
||||
});
|
||||
main.source = self.source.clone();
|
||||
main.root = self
|
||||
.documents
|
||||
.values()
|
||||
.find(|document| document.source == self.source)
|
||||
.and_then(|document| document.root.clone())
|
||||
.or(main.root);
|
||||
main.windows = self
|
||||
.documents
|
||||
.iter()
|
||||
.filter(|(id, _)| **id != crate::window::MAIN_WINDOW)
|
||||
.map(|(id, document)| (*id, document.clone()))
|
||||
.collect();
|
||||
main.validate()?;
|
||||
Ok(Some(main))
|
||||
})();
|
||||
self.bridge.select_window(crate::window::MAIN_WINDOW)?;
|
||||
if result.is_err() && mutated_mounts {
|
||||
self.faulted = true;
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn poll_refresh(&mut self, record_yield: bool) -> Result<RefreshProgress> {
|
||||
let mut work = self.refresh.take().unwrap_or_else(|| RefreshWork {
|
||||
documents: self.documents.clone(),
|
||||
windows: Default::default(),
|
||||
current: None,
|
||||
root: None,
|
||||
pending: None,
|
||||
pass_start: true,
|
||||
pass_changed: false,
|
||||
changed: false,
|
||||
work: 0,
|
||||
});
|
||||
let result = (|| {
|
||||
if self.step_refresh(&mut work)? {
|
||||
return Ok(true);
|
||||
}
|
||||
if record_yield {
|
||||
self.bridge.note_refresh_yield()?;
|
||||
}
|
||||
Ok(false)
|
||||
})();
|
||||
match result {
|
||||
Ok(false) => {
|
||||
self.refresh = Some(work);
|
||||
Ok(RefreshProgress::Pending)
|
||||
}
|
||||
Ok(true) => {
|
||||
let result = self.finish_refresh(work);
|
||||
self.bridge.select_window(crate::window::MAIN_WINDOW)?;
|
||||
if result.is_err() {
|
||||
self.faulted = true;
|
||||
}
|
||||
result.map(RefreshProgress::Complete)
|
||||
}
|
||||
Err(error) => {
|
||||
self.faulted = true;
|
||||
if let Some(pending) = work.pending.take() {
|
||||
self.bridge
|
||||
.abort_layout_frame(&mut self.candidate.vm, *pending)?;
|
||||
}
|
||||
self.bridge.select_window(crate::window::MAIN_WINDOW)?;
|
||||
Err(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
work.pass_changed = false;
|
||||
work.pass_start = false;
|
||||
return Ok(false);
|
||||
}
|
||||
let Some(id) = work.current else {
|
||||
if let Some(id) = work.windows.pop_front() {
|
||||
work.current = Some(id);
|
||||
return Ok(false);
|
||||
}
|
||||
if work.pass_changed {
|
||||
work.pass_start = true;
|
||||
return Ok(false);
|
||||
}
|
||||
return Ok(true);
|
||||
};
|
||||
self.bridge.select_window(id)?;
|
||||
let progress = if let Some(pending) = work.pending.take() {
|
||||
self.bridge
|
||||
.resume_layout_frame(&mut self.candidate.vm, *pending)?
|
||||
} else if let Some(root) = self.bridge.prepare_frame(&mut self.candidate.vm)? {
|
||||
work.root = root.clone();
|
||||
self.bridge
|
||||
.begin_layout_frame(&mut self.candidate.vm, root)?
|
||||
} else {
|
||||
work.current = None;
|
||||
return Ok(false);
|
||||
};
|
||||
let frame = match progress {
|
||||
crate::ui_bridge::LayoutProgress::Pending(pending) => {
|
||||
work.pending = Some(pending);
|
||||
return Ok(false);
|
||||
}
|
||||
crate::ui_bridge::LayoutProgress::Complete(frame) => *frame,
|
||||
};
|
||||
let mut document = RenderDocument {
|
||||
windows: Default::default(),
|
||||
source: self.source.clone(),
|
||||
root: Some(self.program_root.clone().unwrap_or_else(|| CodeNode {
|
||||
id: format!("console-session-{}", self.id),
|
||||
name: format!("Console session {}", self.id),
|
||||
kind: "repl-session".into(),
|
||||
start_line: 1,
|
||||
end_line: self.source.lines().count(),
|
||||
children: self.nodes.clone(),
|
||||
})),
|
||||
frame,
|
||||
ui_root: work.root.take(),
|
||||
};
|
||||
document.frame.session = self.id;
|
||||
document.validate()?;
|
||||
work.work += document.frame.mounts.len().max(1);
|
||||
if work.work > 200_000 {
|
||||
bail!("layout dependencies did not stabilize within the rendering work budget");
|
||||
}
|
||||
self.bridge.paint_frame(
|
||||
&mut self.candidate.vm,
|
||||
document.ui_root.as_deref(),
|
||||
&mut document.frame,
|
||||
)?;
|
||||
document.validate()?;
|
||||
work.documents.insert(id, document);
|
||||
work.pass_changed = true;
|
||||
work.changed = true;
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
fn finish_refresh(&mut self, mut work: RefreshWork) -> Result<Option<RenderDocument>> {
|
||||
self.bridge.finish_closed(&mut self.candidate.vm)?;
|
||||
let ids = self.bridge.window_ids()?;
|
||||
work.documents.retain(|id, _| ids.contains(id));
|
||||
if !work.changed {
|
||||
self.documents = work.documents;
|
||||
return Ok(None);
|
||||
}
|
||||
let mut main = work
|
||||
.documents
|
||||
.get(&crate::window::MAIN_WINDOW)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| RenderDocument {
|
||||
source: self.source.clone(),
|
||||
frame: crate::document::Frame {
|
||||
session: self.id,
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
main.source = self.source.clone();
|
||||
main.root = work
|
||||
.documents
|
||||
.values()
|
||||
.find(|document| document.source == self.source)
|
||||
.and_then(|document| document.root.clone())
|
||||
.or(main.root);
|
||||
main.windows = work
|
||||
.documents
|
||||
.iter()
|
||||
.filter(|(id, _)| **id != crate::window::MAIN_WINDOW)
|
||||
.map(|(id, document)| (*id, document.clone()))
|
||||
.collect();
|
||||
main.validate()?;
|
||||
self.documents = work.documents;
|
||||
Ok(Some(main))
|
||||
}
|
||||
|
||||
pub fn resize_windows(
|
||||
&mut self,
|
||||
viewports: &std::collections::BTreeMap<u64, crate::layout::Size>,
|
||||
) -> Result<Option<RenderDocument>> {
|
||||
self.resize_windows_incremental(viewports, true)
|
||||
}
|
||||
|
||||
pub fn resize_windows_incremental(
|
||||
&mut self,
|
||||
viewports: &std::collections::BTreeMap<u64, crate::layout::Size>,
|
||||
drain: bool,
|
||||
) -> Result<Option<RenderDocument>> {
|
||||
if self.faulted {
|
||||
bail!("runtime stopped after a callback or rendering failure");
|
||||
}
|
||||
let previous = if self.has_pending_refresh() {
|
||||
if drain {
|
||||
let result = self.refresh_document();
|
||||
self.finish_refresh_update(result)?
|
||||
} else {
|
||||
return self.advance_idle_refresh();
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let ids = self.bridge.window_ids()?;
|
||||
if ids.len() == 1
|
||||
&& let Some(viewport) = viewports.get(&crate::window::MAIN_WINDOW)
|
||||
{
|
||||
self.bridge.select_window(crate::window::MAIN_WINDOW)?;
|
||||
return self.resize(Some(*viewport));
|
||||
}
|
||||
let mut changed = false;
|
||||
for (id, viewport) in viewports {
|
||||
if ids.contains(id) {
|
||||
@@ -592,15 +695,38 @@ impl ReplSession {
|
||||
}
|
||||
self.bridge.select_window(crate::window::MAIN_WINDOW)?;
|
||||
if !changed {
|
||||
return Ok(None);
|
||||
return Ok(previous);
|
||||
}
|
||||
let result = self.refresh_document();
|
||||
if drain {
|
||||
let result = self
|
||||
.refresh_document()
|
||||
.map(|document| document.or(previous));
|
||||
self.finish_refresh_update(result)
|
||||
} else {
|
||||
self.advance_idle_refresh()
|
||||
}
|
||||
}
|
||||
|
||||
fn advance_idle_refresh(&mut self) -> Result<Option<RenderDocument>> {
|
||||
let result = self.poll_refresh(true).map(|progress| match progress {
|
||||
RefreshProgress::Pending => None,
|
||||
RefreshProgress::Complete(document) => document,
|
||||
});
|
||||
self.finish_refresh_update(result)
|
||||
}
|
||||
|
||||
fn finish_refresh_update(
|
||||
&mut self,
|
||||
result: Result<Option<RenderDocument>>,
|
||||
) -> Result<Option<RenderDocument>> {
|
||||
if result.is_err() {
|
||||
self.take_window_commands()?;
|
||||
self.bridge.discard_pending(&mut self.candidate.vm)?;
|
||||
}
|
||||
self.bridge
|
||||
.reclaim(&mut self.candidate.vm, &mut self.candidate.roots)?;
|
||||
if !self.has_pending_refresh() {
|
||||
self.bridge
|
||||
.reclaim(&mut self.candidate.vm, &mut self.candidate.roots)?;
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
|
||||
@@ -228,6 +228,7 @@ struct Session {
|
||||
next_window: u64,
|
||||
next_mount: u64,
|
||||
retired_commands: Vec<crate::window::Command>,
|
||||
refresh_yields: usize,
|
||||
}
|
||||
|
||||
impl std::ops::Deref for Session {
|
||||
@@ -1153,6 +1154,7 @@ impl NativeCallable for Function {
|
||||
("arranged", session.layout.arranged as i64),
|
||||
("layoutPasses", session.layout.settle_passes as i64),
|
||||
("layoutSlices", session.layout.slices as i64),
|
||||
("refreshYields", session.refresh_yields as i64),
|
||||
] {
|
||||
cx.set_attr(stats, name, Value::Number(value.into()))?;
|
||||
}
|
||||
@@ -2590,6 +2592,26 @@ impl UiBridge {
|
||||
self.accept_layout_progress(vm, context, progress)
|
||||
}
|
||||
|
||||
pub fn note_refresh_yield(&self) -> Result<()> {
|
||||
let mut state = self
|
||||
.session
|
||||
.lock()
|
||||
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
|
||||
state.refresh_yields = state.refresh_yields.saturating_add(1);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Abort is currently a fault-domain cleanup operation, not a live rollback
|
||||
// of Mount preparation. The caller preserves the last published picture.
|
||||
pub fn abort_layout_frame(&self, vm: &mut Vm, pending: PendingLayout) -> Result<()> {
|
||||
let PendingLayout { work, context } = pending;
|
||||
let cancelled = work.cancel(vm);
|
||||
let released = context.hooks.map(|hooks| vm.release(hooks)).transpose();
|
||||
cancelled?;
|
||||
released?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn resume_layout_frame(
|
||||
&self,
|
||||
vm: &mut Vm,
|
||||
@@ -2760,6 +2782,7 @@ pub fn install(vm: &mut Vm, roots: &mut Vec<ObjectRef>) -> Result<UiBridge> {
|
||||
next_window: crate::window::MAIN_WINDOW,
|
||||
next_mount: 0,
|
||||
retired_commands: Vec::new(),
|
||||
refresh_yields: 0,
|
||||
}));
|
||||
let mut public_functions = Vec::new();
|
||||
let mut factories = Vec::new();
|
||||
|
||||
Reference in New Issue
Block a user