From ba26e74beafd2de719e89dba2199e2aa7b581854 Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Fri, 23 Oct 2026 01:20:39 +0800 Subject: [PATCH] feat: park viewport refreshes across worker turns and serialize new input --- README.md | 3 +++ src/README.md | 3 +++ src/api.rs | 37 ++++++++++++++++++++++++++++++++++--- 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c8e7d52..0126c5a 100644 --- a/README.md +++ b/README.md @@ -509,3 +509,6 @@ UiBridge::begin_layout_frame/resume_layout_frame now expose PendingLayout around 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. + + +Viewport refresh now advances through ReplSession's saved refresh state on worker idle turns. A turn advances one traversal stage or a 256-instruction layout quantum; a pending job shortens the next worker wait to 1 ms. Tasks/window commands and document publication wait for complete refresh. ui.stats().refreshYields reports cooperative idle returns. Mutating requests first drain the existing job, so they cannot run inside its read-only scope. This is serialized input, not live cancellation/rollback or latest-input coalescing. Initial loads, explicit REPL/event refresh and task callbacks still drain synchronously; native Rust measurement/paint calls remain atomic. diff --git a/src/README.md b/src/README.md index 18252e2..36887d7 100644 --- a/src/README.md +++ b/src/README.md @@ -161,3 +161,6 @@ UiBridge::begin_layout_frame/resume_layout_frame now expose PendingLayout around 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. + + +Viewport refresh now advances through ReplSession's saved refresh state on worker idle turns. A turn advances one traversal stage or a 256-instruction layout quantum; a pending job shortens the next worker wait to 1 ms. Tasks/window commands and document publication wait for complete refresh. ui.stats().refreshYields reports cooperative idle returns. Mutating requests first drain the existing job, so they cannot run inside its read-only scope. This is serialized input, not live cancellation/rollback or latest-input coalescing. Initial loads, explicit REPL/event refresh and task callbacks still drain synchronously; native Rust measurement/paint calls remain atomic. diff --git a/src/api.rs b/src/api.rs index 9d36008..fff05cf 100644 --- a/src/api.rs +++ b/src/api.rs @@ -94,7 +94,16 @@ fn start_server_observed( let mut worker = Worker::default(); loop { worker.refresh_viewport(&worker_namespace, &worker_notify); - match receiver.recv_timeout(std::time::Duration::from_millis(100)) { + let wait = if worker + .repl + .as_ref() + .is_some_and(|(_, runtime)| runtime.has_pending_refresh()) + { + std::time::Duration::from_millis(1) + } else { + std::time::Duration::from_millis(100) + }; + match receiver.recv_timeout(wait) { Ok(job) => { let response = worker.run(&job, &worker_namespace, &worker_notify); let _ = job.request.respond(response); @@ -503,6 +512,15 @@ impl Worker { } fn refresh_viewport(&mut self, namespace: &Arc>, notify: &Notify) { + self.refresh_viewport_mode(namespace, notify, false); + } + + fn refresh_viewport_mode( + &mut self, + namespace: &Arc>, + notify: &Notify, + drain: bool, + ) { let (epoch, viewports) = match namespace.read() { Ok(current) => { let mut viewports: std::collections::BTreeMap<_, _> = current @@ -537,7 +555,14 @@ impl Worker { runtime.poll_tasks()?; return Ok((None, Vec::new())); } - let resized = runtime.resize_windows(&viewports)?; + let resized = if drain { + runtime.resize_windows(&viewports)? + } else { + runtime.resize_windows_incremental(&viewports, false)? + }; + if runtime.has_pending_refresh() { + return Ok((None, Vec::new())); + } let completed = runtime.poll_tasks()?; Ok((completed.or(resized), runtime.take_window_commands()?)) })); @@ -604,7 +629,10 @@ impl Worker { if job.kind == RequestKind::Expression { return build(job, namespace, notify); } - self.refresh_viewport(namespace, notify); + // Mutating requests serialize behind a pending refresh. Idle polling + // parks between quanta, but input never interleaves heap mutation with + // an active read-only call or publishes a partly refreshed window set. + self.refresh_viewport_mode(namespace, notify, true); let epoch = match namespace.read() { Ok(current) => current.submission, Err(_) => return error_response(StatusCode(500), "namespace lock is poisoned"), @@ -1136,6 +1164,9 @@ 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. + +Viewport refresh now advances through ReplSession's saved refresh state on worker idle turns. A turn advances one traversal stage or a 256-instruction layout quantum; a pending job shortens the next worker wait to 1 ms. Tasks/window commands and document publication wait for complete refresh. ui.stats().refreshYields reports cooperative idle returns. Mutating requests first drain the existing job, so they cannot run inside its read-only scope. This is serialized input, not live cancellation/rollback or latest-input coalescing. Initial loads, explicit REPL/event refresh and task callbacks still drain synchronously; native Rust measurement/paint calls remain atomic. + ## UI host object Positional arguments only: