From ed4fb23bd6f8eb8c90ba705e5f884814b733daa6 Mon Sep 17 00:00:00 2001 From: Aditya Sharma Date: Thu, 16 Jul 2026 17:42:41 -0700 Subject: [PATCH] refactor: replace AppProxy poll loop with MainThreadPoster AppProxy scheduled cross-thread work onto the main thread by draining an mpsc channel from a foreground task that woke every 16ms via a background timer. That poll loop was flagged as a wart: an idle tray app paid a fixed wakeup cost forever, purely because the fork had no dependency-free way to wake the main thread from a Send context. The fork now provides that primitive. App::main_thread_poster hands back a Clone+Send+Sync MainThreadPoster whose post() enqueues a closure and wakes the run loop through the platform dispatcher, so an idle app stays parked. Back AppProxy with the poster and delete the timer loop. The shutdown boundary stays ours: a `closed` AtomicBool, set at ShutdownRequested (which precedes app teardown), is checked in dispatch and re-checked inside each posted closure on the main thread. Since close() also runs on the main thread, any closure the pump dispatches after close() drops its payload, preserving the old drain loop's discard-after-close contract. The poster's own after-teardown false is a second layer, not a replacement. Pins gpui/gpui_platform and the vendor submodule to 2ee4fa6 (gpui#7). The workspace version and COMPATIBILITY.md bump is deferred to merge time via /update-gpui discipline. --- Cargo.lock | 46 ++++----- Cargo.toml | 4 +- crates/app/Cargo.toml | 1 + crates/app/src/handles.rs | 203 ++++++++++++++++++++++---------------- vendor/gpui | 2 +- 5 files changed, 146 insertions(+), 110 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ae3a0d7a..b96e9a7c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1276,7 +1276,7 @@ dependencies = [ [[package]] name = "collections" version = "0.1.0" -source = "git+https://github.com/BumpyClock/gpui?rev=4332ea7deae4838c12bad6ea64292ca22a33cf98#4332ea7deae4838c12bad6ea64292ca22a33cf98" +source = "git+https://github.com/BumpyClock/gpui?rev=2ee4fa6587f9fc8eef012f86897034cb7d859d9d#2ee4fa6587f9fc8eef012f86897034cb7d859d9d" dependencies = [ "gpui_util", "indexmap", @@ -1830,7 +1830,7 @@ dependencies = [ [[package]] name = "derive_refineable" version = "0.1.0" -source = "git+https://github.com/BumpyClock/gpui?rev=4332ea7deae4838c12bad6ea64292ca22a33cf98#4332ea7deae4838c12bad6ea64292ca22a33cf98" +source = "git+https://github.com/BumpyClock/gpui?rev=2ee4fa6587f9fc8eef012f86897034cb7d859d9d#2ee4fa6587f9fc8eef012f86897034cb7d859d9d" dependencies = [ "proc-macro2", "quote", @@ -3021,7 +3021,7 @@ dependencies = [ [[package]] name = "gpui" version = "0.2.2" -source = "git+https://github.com/BumpyClock/gpui?rev=4332ea7deae4838c12bad6ea64292ca22a33cf98#4332ea7deae4838c12bad6ea64292ca22a33cf98" +source = "git+https://github.com/BumpyClock/gpui?rev=2ee4fa6587f9fc8eef012f86897034cb7d859d9d#2ee4fa6587f9fc8eef012f86897034cb7d859d9d" dependencies = [ "accesskit", "anyhow", @@ -3291,7 +3291,7 @@ dependencies = [ [[package]] name = "gpui_linux" version = "0.1.0" -source = "git+https://github.com/BumpyClock/gpui?rev=4332ea7deae4838c12bad6ea64292ca22a33cf98#4332ea7deae4838c12bad6ea64292ca22a33cf98" +source = "git+https://github.com/BumpyClock/gpui?rev=2ee4fa6587f9fc8eef012f86897034cb7d859d9d#2ee4fa6587f9fc8eef012f86897034cb7d859d9d" dependencies = [ "accesskit", "accesskit_unix", @@ -3345,7 +3345,7 @@ dependencies = [ [[package]] name = "gpui_macos" version = "0.1.0" -source = "git+https://github.com/BumpyClock/gpui?rev=4332ea7deae4838c12bad6ea64292ca22a33cf98#4332ea7deae4838c12bad6ea64292ca22a33cf98" +source = "git+https://github.com/BumpyClock/gpui?rev=2ee4fa6587f9fc8eef012f86897034cb7d859d9d#2ee4fa6587f9fc8eef012f86897034cb7d859d9d" dependencies = [ "accesskit", "accesskit_macos", @@ -3392,7 +3392,7 @@ dependencies = [ [[package]] name = "gpui_macros" version = "0.1.0" -source = "git+https://github.com/BumpyClock/gpui?rev=4332ea7deae4838c12bad6ea64292ca22a33cf98#4332ea7deae4838c12bad6ea64292ca22a33cf98" +source = "git+https://github.com/BumpyClock/gpui?rev=2ee4fa6587f9fc8eef012f86897034cb7d859d9d#2ee4fa6587f9fc8eef012f86897034cb7d859d9d" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -3403,7 +3403,7 @@ dependencies = [ [[package]] name = "gpui_platform" version = "0.1.0" -source = "git+https://github.com/BumpyClock/gpui?rev=4332ea7deae4838c12bad6ea64292ca22a33cf98#4332ea7deae4838c12bad6ea64292ca22a33cf98" +source = "git+https://github.com/BumpyClock/gpui?rev=2ee4fa6587f9fc8eef012f86897034cb7d859d9d#2ee4fa6587f9fc8eef012f86897034cb7d859d9d" dependencies = [ "console_error_panic_hook", "gpui", @@ -3416,7 +3416,7 @@ dependencies = [ [[package]] name = "gpui_shared_string" version = "0.1.0" -source = "git+https://github.com/BumpyClock/gpui?rev=4332ea7deae4838c12bad6ea64292ca22a33cf98#4332ea7deae4838c12bad6ea64292ca22a33cf98" +source = "git+https://github.com/BumpyClock/gpui?rev=2ee4fa6587f9fc8eef012f86897034cb7d859d9d#2ee4fa6587f9fc8eef012f86897034cb7d859d9d" dependencies = [ "schemars", "serde", @@ -3426,7 +3426,7 @@ dependencies = [ [[package]] name = "gpui_util" version = "0.1.0" -source = "git+https://github.com/BumpyClock/gpui?rev=4332ea7deae4838c12bad6ea64292ca22a33cf98#4332ea7deae4838c12bad6ea64292ca22a33cf98" +source = "git+https://github.com/BumpyClock/gpui?rev=2ee4fa6587f9fc8eef012f86897034cb7d859d9d#2ee4fa6587f9fc8eef012f86897034cb7d859d9d" dependencies = [ "anyhow", "log", @@ -3436,7 +3436,7 @@ dependencies = [ [[package]] name = "gpui_web" version = "0.1.0" -source = "git+https://github.com/BumpyClock/gpui?rev=4332ea7deae4838c12bad6ea64292ca22a33cf98#4332ea7deae4838c12bad6ea64292ca22a33cf98" +source = "git+https://github.com/BumpyClock/gpui?rev=2ee4fa6587f9fc8eef012f86897034cb7d859d9d#2ee4fa6587f9fc8eef012f86897034cb7d859d9d" dependencies = [ "anyhow", "console_error_panic_hook", @@ -3460,7 +3460,7 @@ dependencies = [ [[package]] name = "gpui_wgpu" version = "0.1.0" -source = "git+https://github.com/BumpyClock/gpui?rev=4332ea7deae4838c12bad6ea64292ca22a33cf98#4332ea7deae4838c12bad6ea64292ca22a33cf98" +source = "git+https://github.com/BumpyClock/gpui?rev=2ee4fa6587f9fc8eef012f86897034cb7d859d9d#2ee4fa6587f9fc8eef012f86897034cb7d859d9d" dependencies = [ "anyhow", "bytemuck", @@ -3489,7 +3489,7 @@ dependencies = [ [[package]] name = "gpui_windows" version = "0.1.0" -source = "git+https://github.com/BumpyClock/gpui?rev=4332ea7deae4838c12bad6ea64292ca22a33cf98#4332ea7deae4838c12bad6ea64292ca22a33cf98" +source = "git+https://github.com/BumpyClock/gpui?rev=2ee4fa6587f9fc8eef012f86897034cb7d859d9d#2ee4fa6587f9fc8eef012f86897034cb7d859d9d" dependencies = [ "accesskit", "accesskit_windows", @@ -3799,7 +3799,7 @@ dependencies = [ [[package]] name = "http_client" version = "0.1.0" -source = "git+https://github.com/BumpyClock/gpui?rev=4332ea7deae4838c12bad6ea64292ca22a33cf98#4332ea7deae4838c12bad6ea64292ca22a33cf98" +source = "git+https://github.com/BumpyClock/gpui?rev=2ee4fa6587f9fc8eef012f86897034cb7d859d9d#2ee4fa6587f9fc8eef012f86897034cb7d859d9d" dependencies = [ "anyhow", "async-compression", @@ -4810,7 +4810,7 @@ dependencies = [ [[package]] name = "media" version = "0.1.0" -source = "git+https://github.com/BumpyClock/gpui?rev=4332ea7deae4838c12bad6ea64292ca22a33cf98#4332ea7deae4838c12bad6ea64292ca22a33cf98" +source = "git+https://github.com/BumpyClock/gpui?rev=2ee4fa6587f9fc8eef012f86897034cb7d859d9d#2ee4fa6587f9fc8eef012f86897034cb7d859d9d" dependencies = [ "anyhow", "bindgen", @@ -5737,7 +5737,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perf" version = "0.1.0" -source = "git+https://github.com/BumpyClock/gpui?rev=4332ea7deae4838c12bad6ea64292ca22a33cf98#4332ea7deae4838c12bad6ea64292ca22a33cf98" +source = "git+https://github.com/BumpyClock/gpui?rev=2ee4fa6587f9fc8eef012f86897034cb7d859d9d#2ee4fa6587f9fc8eef012f86897034cb7d859d9d" dependencies = [ "collections", "serde", @@ -6727,7 +6727,7 @@ dependencies = [ [[package]] name = "refineable" version = "0.1.0" -source = "git+https://github.com/BumpyClock/gpui?rev=4332ea7deae4838c12bad6ea64292ca22a33cf98#4332ea7deae4838c12bad6ea64292ca22a33cf98" +source = "git+https://github.com/BumpyClock/gpui?rev=2ee4fa6587f9fc8eef012f86897034cb7d859d9d#2ee4fa6587f9fc8eef012f86897034cb7d859d9d" dependencies = [ "derive_refineable", ] @@ -7181,7 +7181,7 @@ dependencies = [ [[package]] name = "scheduler" version = "0.1.0" -source = "git+https://github.com/BumpyClock/gpui?rev=4332ea7deae4838c12bad6ea64292ca22a33cf98#4332ea7deae4838c12bad6ea64292ca22a33cf98" +source = "git+https://github.com/BumpyClock/gpui?rev=2ee4fa6587f9fc8eef012f86897034cb7d859d9d#2ee4fa6587f9fc8eef012f86897034cb7d859d9d" dependencies = [ "async-task", "backtrace", @@ -7814,7 +7814,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "sum_tree" version = "0.1.0" -source = "git+https://github.com/BumpyClock/gpui?rev=4332ea7deae4838c12bad6ea64292ca22a33cf98#4332ea7deae4838c12bad6ea64292ca22a33cf98" +source = "git+https://github.com/BumpyClock/gpui?rev=2ee4fa6587f9fc8eef012f86897034cb7d859d9d#2ee4fa6587f9fc8eef012f86897034cb7d859d9d" dependencies = [ "heapless", "log", @@ -9192,7 +9192,7 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "util" version = "0.1.0" -source = "git+https://github.com/BumpyClock/gpui?rev=4332ea7deae4838c12bad6ea64292ca22a33cf98#4332ea7deae4838c12bad6ea64292ca22a33cf98" +source = "git+https://github.com/BumpyClock/gpui?rev=2ee4fa6587f9fc8eef012f86897034cb7d859d9d#2ee4fa6587f9fc8eef012f86897034cb7d859d9d" dependencies = [ "anyhow", "async-fs", @@ -9231,7 +9231,7 @@ dependencies = [ [[package]] name = "util_macros" version = "0.1.0" -source = "git+https://github.com/BumpyClock/gpui?rev=4332ea7deae4838c12bad6ea64292ca22a33cf98#4332ea7deae4838c12bad6ea64292ca22a33cf98" +source = "git+https://github.com/BumpyClock/gpui?rev=2ee4fa6587f9fc8eef012f86897034cb7d859d9d#2ee4fa6587f9fc8eef012f86897034cb7d859d9d" dependencies = [ "perf", "quote", @@ -11164,7 +11164,7 @@ dependencies = [ [[package]] name = "zlog" version = "0.1.0" -source = "git+https://github.com/BumpyClock/gpui?rev=4332ea7deae4838c12bad6ea64292ca22a33cf98#4332ea7deae4838c12bad6ea64292ca22a33cf98" +source = "git+https://github.com/BumpyClock/gpui?rev=2ee4fa6587f9fc8eef012f86897034cb7d859d9d#2ee4fa6587f9fc8eef012f86897034cb7d859d9d" dependencies = [ "anyhow", "chrono", @@ -11181,7 +11181,7 @@ checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" [[package]] name = "ztracing" version = "0.1.0" -source = "git+https://github.com/BumpyClock/gpui?rev=4332ea7deae4838c12bad6ea64292ca22a33cf98#4332ea7deae4838c12bad6ea64292ca22a33cf98" +source = "git+https://github.com/BumpyClock/gpui?rev=2ee4fa6587f9fc8eef012f86897034cb7d859d9d#2ee4fa6587f9fc8eef012f86897034cb7d859d9d" dependencies = [ "tracing", "tracing-subscriber", @@ -11192,7 +11192,7 @@ dependencies = [ [[package]] name = "ztracing_macro" version = "0.1.0" -source = "git+https://github.com/BumpyClock/gpui?rev=4332ea7deae4838c12bad6ea64292ca22a33cf98#4332ea7deae4838c12bad6ea64292ca22a33cf98" +source = "git+https://github.com/BumpyClock/gpui?rev=2ee4fa6587f9fc8eef012f86897034cb7d859d9d#2ee4fa6587f9fc8eef012f86897034cb7d859d9d" [[package]] name = "zune-core" diff --git a/Cargo.toml b/Cargo.toml index fcf7e401..6eafbabb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,8 +37,8 @@ gpui-component-macros = { path = "crates/macros", version = "0.5.1" } gpui-component-assets = { path = "crates/assets", version = "0.5.1" } story = { path = "crates/story" } -gpui = { git = "https://github.com/BumpyClock/gpui", rev = "4332ea7deae4838c12bad6ea64292ca22a33cf98", features = ["font-kit"] } -gpui_platform = { git = "https://github.com/BumpyClock/gpui", rev = "4332ea7deae4838c12bad6ea64292ca22a33cf98", features = ["font-kit"] } +gpui = { git = "https://github.com/BumpyClock/gpui", rev = "2ee4fa6587f9fc8eef012f86897034cb7d859d9d", features = ["font-kit"] } +gpui_platform = { git = "https://github.com/BumpyClock/gpui", rev = "2ee4fa6587f9fc8eef012f86897034cb7d859d9d", features = ["font-kit"] } gpui-macros = "0.2.2" sum-tree = { version = "0.2.0", package = "zed-sum-tree" } # reqwest = { version = "0.12.15-zed", package = "zed-reqwest" } diff --git a/crates/app/Cargo.toml b/crates/app/Cargo.toml index ac8949be..cf15b271 100644 --- a/crates/app/Cargo.toml +++ b/crates/app/Cargo.toml @@ -19,6 +19,7 @@ thiserror.workspace = true toml.workspace = true [dev-dependencies] +gpui = { workspace = true, features = ["test-support"] } serde_json.workspace = true tempfile.workspace = true diff --git a/crates/app/src/handles.rs b/crates/app/src/handles.rs index fdea5bf4..f2707a15 100644 --- a/crates/app/src/handles.rs +++ b/crates/app/src/handles.rs @@ -12,11 +12,10 @@ use std::any::{Any, TypeId}; use std::collections::HashMap; use std::collections::VecDeque; -use std::sync::mpsc::{Receiver, Sender, channel}; +use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex, OnceLock}; -use std::time::Duration; -use gpui::App; +use gpui::{App, MainThreadPoster}; use gpui_component_manifest::schema::IdentityRef; use gpui_component_storage::AppPaths; @@ -27,14 +26,6 @@ use crate::liveness::{Liveness, ShellHold}; use crate::phases::PhaseTracker; use crate::plugin::{AppPlugin, EventHandler}; -/// How long the cross-thread drain loop sleeps between polls. -/// -/// WART (flagged): the gpui fork exposes no dependency-free way to wake the main -/// thread from a `Send` context (see the report). The proxy therefore *polls* -/// its command channel on the foreground executor. A proper fix is a gpui -/// `spawn_on_main(impl FnOnce(&mut App) + Send)` primitive or an async channel. -const PROXY_POLL_INTERVAL: Duration = Duration::from_millis(16); - /// Immutable application identity, resolved paths, and capability snapshot. /// /// `Clone + Send + Sync` (compile-asserted below) so background threads can read @@ -96,61 +87,79 @@ impl AppInfo { } } -type ProxyCommand = Box; - /// Cross-thread dispatch handle: schedules work onto the main thread. /// /// `Clone + Send + Sync`. Serves tray/hotkey/watcher/audio callbacks alike. /// After shutdown begins, [`AppProxy::dispatch`] returns [`AppClosed`]. +/// +/// Backed by the gpui fork's [`MainThreadPoster`]: posting wakes the main run +/// loop through the platform dispatcher, so an idle app stays parked (no poll +/// loop). The `closed` flag is our own shutdown boundary, set at +/// `ShutdownRequested` — which precedes app teardown, so it fires strictly +/// before the poster's own after-teardown rejection (see [`AppProxy::dispatch`]). #[derive(Clone)] pub struct AppProxy { inner: Arc, } struct ProxyInner { - /// `None` once closed. The closed state lives *inside* the mutex so the - /// closed-check and the send are one atomic operation — nothing can be - /// accepted after `close()` wins the lock. - sender: Mutex>>, + poster: MainThreadPoster, + /// Set at the `ShutdownRequested` boundary. Two roles: `dispatch` rejects + /// new work once it is set, and every posted closure re-checks it on the + /// main thread before running so work already queued behind a callback that + /// triggered shutdown is discarded rather than run mid-teardown. + closed: AtomicBool, } impl AppProxy { - /// Create a proxy and its receiver. The receiver is consumed by the - /// main-thread drain loop; the proxy is cloned to background consumers. - fn new() -> (Self, Receiver) { - let (tx, rx) = channel(); - let inner = Arc::new(ProxyInner { - sender: Mutex::new(Some(tx)), - }); - (Self { inner }, rx) + /// Create a proxy backed by the app's [`MainThreadPoster`]. Cloned to + /// background consumers; posts run on the main thread via the app's pump. + fn new(cx: &mut App) -> Self { + Self { + inner: Arc::new(ProxyInner { + poster: cx.main_thread_poster(), + closed: AtomicBool::new(false), + }), + } } /// Schedule `f` to run on the main thread with `&mut App`. /// - /// Returns [`AppClosed`] once shutdown has begun (the proxy is closed at the - /// `ShutdownRequested` boundary). The closed-check and send are atomic under - /// one lock, so no callback is enqueued past that boundary. + /// Returns [`AppClosed`] once shutdown has begun — the proxy is closed at the + /// `ShutdownRequested` boundary, which precedes app teardown. Two layers + /// enforce the boundary: + /// + /// - a fast pre-check rejects if `closed` is already visible here, and + /// - the posted closure re-checks `closed` on the main thread before running + /// `f`. Since `close()` also runs on the main thread, any closure the pump + /// dispatches after `close()` sees the flag and drops its payload. So a + /// dispatch that races `close()` may return `Ok`, but its `f` never runs + /// past the boundary — matching the old drain-loop's discard-after-close. + /// + /// A `false` from the poster (the main-thread receiver is gone, i.e. the app + /// has fully torn down past `close()`) also maps to [`AppClosed`]. pub fn dispatch(&self, f: impl FnOnce(&mut App) + Send + 'static) -> Result<(), AppClosed> { - let guard = self.inner.sender.lock().expect("proxy sender poisoned"); - match guard.as_ref() { - Some(tx) => tx.send(Box::new(f)).map_err(|_| AppClosed), - None => Err(AppClosed), + if self.inner.closed.load(Ordering::Acquire) { + return Err(AppClosed); } + let inner = Arc::clone(&self.inner); + let posted = self.inner.poster.post(move |app| { + if !inner.closed.load(Ordering::Acquire) { + f(app); + } + }); + if posted { Ok(()) } else { Err(AppClosed) } } /// Whether the proxy has been closed. pub fn is_closed(&self) -> bool { - self.inner - .sender - .lock() - .expect("proxy sender poisoned") - .is_none() + self.inner.closed.load(Ordering::Acquire) } - /// Close the proxy: reject all future dispatches and let the drain loop - /// observe disconnection and exit. Idempotent. + /// Close the proxy: reject future dispatches and cause already-queued posts + /// to discard their payload when the pump reaches them. Idempotent. fn close(&self) { - *self.inner.sender.lock().expect("proxy sender poisoned") = None; + self.inner.closed.store(true, Ordering::Release); } } @@ -195,7 +204,6 @@ pub struct ShellState { pending_exit_reason: Option, shutdown_requested: bool, will_exit_done: bool, - _drain_task: gpui::Task<()>, } impl gpui::Global for ShellState {} @@ -289,7 +297,8 @@ impl AppShellExt for App { } } -/// Install the shell global and start the cross-thread drain loop. +/// Install the shell global. Wires the cross-thread proxy to the app's +/// [`MainThreadPoster`]; no poll loop — posts wake the main run loop directly. /// /// Called during the `CoreServices` phase with the constructed `AppInfo` and the /// plugins/handlers/state accumulated by the builder. @@ -304,8 +313,7 @@ pub(crate) fn install( state: HashMap>, phases: PhaseTracker, ) -> AppProxy { - let (proxy, rx) = AppProxy::new(); - let drain_task = spawn_drain_loop(cx, proxy.clone(), rx); + let proxy = AppProxy::new(cx); cx.set_global(ShellState { app_info, @@ -321,38 +329,10 @@ pub(crate) fn install( pending_exit_reason: None, shutdown_requested: false, will_exit_done: false, - _drain_task: drain_task, }); proxy } -fn spawn_drain_loop(cx: &App, proxy: AppProxy, rx: Receiver) -> gpui::Task<()> { - cx.spawn(async move |cx| { - loop { - // Drain queued commands, applying each on the main thread. Re-check - // closed state BETWEEN callbacks: if one triggers shutdown (closing - // the proxy), discard everything still queued behind it rather than - // running it mid-teardown. - loop { - match rx.try_recv() { - Ok(cmd) => { - cx.update(|app| cmd(app)); - if proxy.is_closed() { - return; - } - } - Err(std::sync::mpsc::TryRecvError::Empty) => break, - Err(std::sync::mpsc::TryRecvError::Disconnected) => return, - } - } - if proxy.is_closed() { - return; - } - cx.background_executor().timer(PROXY_POLL_INTERVAL).await; - } - }) -} - /// Register lifecycle observers (window-closed, app-quit) after readiness. pub(crate) fn register_observers(cx: &mut App) { let window_closed = cx.on_window_closed(|app| { @@ -587,24 +567,79 @@ const _: fn() = || { #[cfg(test)] mod tests { use super::*; + use gpui::TestAppContext; + + /// Posted work runs on the main thread; once closed, `dispatch` rejects. + #[gpui::test] + fn dispatch_runs_until_closed(cx: &mut TestAppContext) { + let proxy = cx.update(AppProxy::new); + let log = Arc::new(Mutex::new(Vec::::new())); - #[test] - fn dispatch_enqueues_until_closed() { - let (proxy, rx) = AppProxy::new(); assert!(!proxy.is_closed()); - proxy.dispatch(|_app| {}).expect("dispatch before close"); - assert!(rx.try_recv().is_ok(), "command should be queued"); + let sink = log.clone(); + proxy + .dispatch(move |_app| sink.lock().unwrap().push(1)) + .expect("dispatch before close"); + cx.run_until_parked(); + assert_eq!( + *log.lock().unwrap(), + vec![1], + "posted work ran on main thread" + ); proxy.close(); assert!(proxy.is_closed()); - assert_eq!(proxy.dispatch(|_app| {}), Err(AppClosed)); + assert_eq!( + proxy.dispatch(|_app| {}), + Err(AppClosed), + "dispatch rejected after close" + ); } - #[test] - fn dispatch_fails_when_receiver_dropped() { - let (proxy, rx) = AppProxy::new(); - drop(rx); - // Sender still present but disconnected → AppClosed. - assert_eq!(proxy.dispatch(|_app| {}), Err(AppClosed)); + /// Work accepted before the boundary is discarded if shutdown wins before + /// the pump drains it (the run-time re-check in the posted closure). + #[gpui::test] + fn queued_work_discarded_after_close(cx: &mut TestAppContext) { + let proxy = cx.update(AppProxy::new); + let log = Arc::new(Mutex::new(Vec::::new())); + + let sink = log.clone(); + proxy + .dispatch(move |_app| sink.lock().unwrap().push(1)) + .expect("dispatch before close"); + proxy.close(); + cx.run_until_parked(); + assert!( + log.lock().unwrap().is_empty(), + "payload queued before close is discarded, not run mid-teardown" + ); + } + + /// A callback that triggers shutdown discards work queued behind it, in FIFO + /// order — the equivalent of the old drain loop's between-callback re-check. + #[gpui::test] + fn callback_shutdown_discards_following_work(cx: &mut TestAppContext) { + let proxy = cx.update(AppProxy::new); + let log = Arc::new(Mutex::new(Vec::::new())); + + let sink = log.clone(); + let closer = proxy.clone(); + proxy + .dispatch(move |_app| { + sink.lock().unwrap().push('a'); + closer.close(); + }) + .expect("first dispatch"); + let sink = log.clone(); + proxy + .dispatch(move |_app| sink.lock().unwrap().push('b')) + .expect("second dispatch"); + + cx.run_until_parked(); + assert_eq!( + *log.lock().unwrap(), + vec!['a'], + "work behind the shutdown-triggering callback is discarded" + ); } } diff --git a/vendor/gpui b/vendor/gpui index 4332ea7d..2ee4fa65 160000 --- a/vendor/gpui +++ b/vendor/gpui @@ -1 +1 @@ -Subproject commit 4332ea7deae4838c12bad6ea64292ca22a33cf98 +Subproject commit 2ee4fa6587f9fc8eef012f86897034cb7d859d9d