diff --git a/crates/agent_servers/src/acp.rs b/crates/agent_servers/src/acp.rs index f3bf1ba82b5a00..4789ec012c42dd 100644 --- a/crates/agent_servers/src/acp.rs +++ b/crates/agent_servers/src/acp.rs @@ -669,9 +669,12 @@ const MINIMUM_SUPPORTED_VERSION: ProtocolVersion = ProtocolVersion::V1; /// dispatch queue via `dispatch_tx`, where they are handled by the /// `handle_*` functions on a GPUI context. The returned future drives the /// connection and completes when the transport closes; callers are expected -/// to spawn it on a background executor and hold the task for the lifetime -/// of the connection. The `connection_tx` oneshot receives the -/// `ConnectionTo` handle as soon as the builder runs its `main_fn`. +/// to poll it in the background and hold the task for the lifetime of the +/// connection. In unoptimized builds each inbound dispatch needs ~0.5 MiB +/// of stack, so poll it on a thread with room to spare (macOS GCD workers' +/// 512 KiB is not enough — see `AcpConnection::stdio`). The `connection_tx` +/// oneshot receives the `ConnectionTo` handle as soon as the builder +/// runs its `main_fn`. fn connect_client_future( name: &'static str, transport: impl agent_client_protocol::ConnectTo + 'static, @@ -925,17 +928,25 @@ impl AcpConnection { }); // `connect_client_future` installs the production handler set and - // hands us back both the connection-future (to run on a background - // executor) and a oneshot receiver that produces the - // `ConnectionTo` once the transport handshake is ready. + // hands us back both the connection-future and a oneshot receiver + // that produces the `ConnectionTo` once the transport + // handshake is ready. The future must be polled on a dedicated + // thread rather than via `background_spawn`: in unoptimized builds + // its dispatch chain needs ~0.5 MiB of stack per inbound message, + // which overflows the fixed 512 KiB stacks of the GCD workers that + // poll background tasks on macOS, crashing dev builds as soon as an + // agent sends its first message. See `spawn_dedicated` for the + // stack guarantee that makes the dedicated thread sufficient. let (connection_tx, connection_rx) = futures::channel::oneshot::channel(); let connection_future = connect_client_future("zed", transport, dispatch_tx.clone(), connection_tx); - let io_task = cx.background_spawn(async move { - if let Err(err) = connection_future.await { - log::error!("ACP connection error: {err}"); - } - }); + let io_task = cx + .background_executor() + .spawn_dedicated(move |_executor| async move { + if let Err(err) = connection_future.await { + log::error!("ACP connection error: {err}"); + } + }); let connection_rx = async move { connection_rx diff --git a/crates/gpui/src/executor.rs b/crates/gpui/src/executor.rs index 92afc696d4536e..785a58e95d89ca 100644 --- a/crates/gpui/src/executor.rs +++ b/crates/gpui/src/executor.rs @@ -84,6 +84,27 @@ impl BackgroundExecutor { self.inner.clone() } + /// Spawn a closure on a fresh session pinned to its own [`SchedulerLocalExecutor`]. + /// The closure runs on a new OS thread under the platform scheduler, or on + /// the test scheduler's loop in tests. + /// + /// Prefer this over [`Self::spawn`] for futures whose polls need more stack + /// than shared background threads guarantee. Dedicated threads get the + /// standard library's default 2 MiB, while `spawn` polls futures on + /// whatever threads the platform dispatcher provides — on macOS those are + /// GCD workers whose stacks are fixed at 512 KiB by the kernel (see `PTH_DEFAULT_STACKSIZE` in + /// ), + /// the tightest background-stack budget of any platform. + #[track_caller] + pub fn spawn_dedicated(&self, f: F) -> Task + where + F: FnOnce(SchedulerLocalExecutor) -> Fut + Send + 'static, + Fut: Future + 'static, + Fut::Output: Send + Sync + 'static, + { + self.inner.spawn_dedicated(f) + } + /// Enqueues the given future to be run to completion on a background thread. #[track_caller] pub fn spawn(&self, future: impl Future + Send + 'static) -> Task