Move Servo IPC off UI thread

Root cause of the post-tab lag: the GPUI 16 ms timer was calling
`WebSurfaceRuntime::ensure_tab` and `tick` on the UI thread, and each
call did a synchronous `serde_json` write plus `read_line` against the
Servo sidecar over stdin/stdout. With even one visible tab, every
frame stalled on cross-process IPC.

Introduce `web_surface_worker.rs` — a per-profile worker thread that
owns the `ServoLiveClient`, drains a coalescing request queue
(latest Ensure/Poll per tab wins, no unbounded growth), and ships
results back through a `std::sync::mpsc` channel. `WebSurfaceRuntime`
now submits work non-blockingly and drains responses in `tick`; the
UI thread never blocks on the sidecar.

Adjacent in-flight cleanup riding along: hardware IOSurface
rendering-context completion (sidecar `live_protocol`,
`hardware_rendering_context`, GPUI BGRA surface shader), CSS viewport
size + device pixel ratio plumbing into `ServoLiveFrame`, and the
Send opt-ins for `CVPixelBuffer`-bearing types so frames can cross
the thread boundary.
This commit is contained in:
2026-05-15 16:41:40 -04:00
parent f4c650c4d8
commit 90c029eddb
29 changed files with 2113 additions and 496 deletions
@@ -8,14 +8,22 @@ use ely_domain::{BrowserTab, ProfileId, SpaceId, TabId, UrlText};
use crate::{
services::ProfileDataMode,
shell::{
WebSurfaceStore,
web_surface_geometry::{WebSurfaceScrollOffset, WebSurfaceSize},
web_surface_state::WebSurfacePendingInput,
web_surface_state::{WebSurfaceInputOutcome, WebSurfacePendingInput},
web_surface_worker::{LiveRuntimeClient, LiveRuntimeClientError},
},
};
use super::*;
use crate::services::servo_live::{ServoLiveEnsureRequest, ServoLiveFrame};
static FAKE_CLOSE_COUNT: AtomicUsize = AtomicUsize::new(0);
static FAKE_ENSURE_COUNT: AtomicUsize = AtomicUsize::new(0);
static IDLE_SKIP_ENSURE_COUNT: AtomicUsize = AtomicUsize::new(0);
static RECOVERY_FACTORY_COUNT: AtomicUsize = AtomicUsize::new(0);
static FAILING_ENSURE_COUNT: AtomicUsize = AtomicUsize::new(0);
#[test]
fn runtime_keeps_independent_clients_for_profile_scopes() -> Result<(), String> {
@@ -47,6 +55,8 @@ fn runtime_keeps_independent_clients_for_profile_scopes() -> Result<(), String>
pending_input(),
)?;
runtime.flush_for_test();
assert_eq!(runtime.client_count_for_test(), 2);
assert_eq!(
runtime.session_scope_for_test(first_tab.id()),
@@ -66,16 +76,114 @@ fn close_tab_removes_session_and_closes_client() -> Result<(), String> {
let tab = web_tab(TabId::new(), ProfileId::new(), "https://example.com/close")?;
runtime.ensure_tab(&tab, surface_size(), ProfileDataMode::Transient, &[], pending_input())?;
runtime.flush_for_test();
runtime.close_tab(tab.id());
runtime.flush_for_test();
assert_eq!(runtime.session_scope_for_test(tab.id()), None);
assert_eq!(FAKE_CLOSE_COUNT.load(Ordering::SeqCst), before + 1);
runtime.close_tab(tab.id());
runtime.flush_for_test();
assert_eq!(FAKE_CLOSE_COUNT.load(Ordering::SeqCst), before + 1);
Ok(())
}
#[test]
fn unchanged_surface_without_input_skips_runtime_ensure() -> Result<(), String> {
IDLE_SKIP_ENSURE_COUNT.store(0, Ordering::SeqCst);
let mut store = WebSurfaceStore::new_with_runtime(WebSurfaceRuntime::new_with_client_factory(
idle_skip_client_factory,
));
let tab = web_tab(TabId::new(), ProfileId::new(), "https://example.com/idle")?;
assert_eq!(
store.record_viewport_size(tab.id(), viewport_bounds(), 1.0),
WebSurfaceInputOutcome::Applied,
);
assert!(store.ensure_surface(&tab, ProfileDataMode::Transient, &[]).changed);
store.flush_runtime_for_test();
assert_eq!(IDLE_SKIP_ENSURE_COUNT.load(Ordering::SeqCst), 1);
assert!(!store.ensure_surface(&tab, ProfileDataMode::Transient, &[]).changed);
store.flush_runtime_for_test();
assert_eq!(IDLE_SKIP_ENSURE_COUNT.load(Ordering::SeqCst), 1);
Ok(())
}
#[test]
fn sidecar_exit_removes_dead_runtime_client() -> Result<(), String> {
RECOVERY_FACTORY_COUNT.store(0, Ordering::SeqCst);
let mut runtime = WebSurfaceRuntime::new_with_client_factory(recovery_client_factory);
let profile = ProfileId::new();
let crashed_tab = web_tab(TabId::new(), profile.clone(), "https://example.com/crash")?;
runtime.ensure_tab(
&crashed_tab,
surface_size(),
ProfileDataMode::Transient,
&[],
pending_input(),
)?;
runtime.flush_for_test();
let frames = runtime.tick(&[crashed_tab.id().clone()]);
assert!(
frames.iter().any(
|frame| matches!(frame, WebSurfaceRuntimeFrame::Failed { tab_id, .. } if tab_id == crashed_tab.id())
),
"the crashed tab must surface as a Failed frame",
);
assert_eq!(runtime.client_count_for_test(), 0);
let next_tab = web_tab(TabId::new(), profile, "https://example.com/next")?;
runtime.ensure_tab(
&next_tab,
surface_size(),
ProfileDataMode::Transient,
&[],
pending_input(),
)?;
runtime.flush_for_test();
assert_eq!(RECOVERY_FACTORY_COUNT.load(Ordering::SeqCst), 2);
assert_eq!(runtime.client_count_for_test(), 1);
Ok(())
}
#[test]
fn failed_surface_ensure_waits_for_a_new_key_before_retrying() -> Result<(), String> {
FAILING_ENSURE_COUNT.store(0, Ordering::SeqCst);
let mut store = WebSurfaceStore::new_with_runtime(WebSurfaceRuntime::new_with_client_factory(
failing_client_factory,
));
let tab = web_tab(TabId::new(), ProfileId::new(), "https://example.com/crash")?;
assert_eq!(
store.record_viewport_size(tab.id(), viewport_bounds(), 1.0),
WebSurfaceInputOutcome::Applied,
);
assert!(store.ensure_surface(&tab, ProfileDataMode::Transient, &[]).changed);
store.flush_runtime_for_test();
let tick = store.tick(&[tab.id().clone()]);
assert!(tick.changed, "the failing client must surface a state change via tick");
assert_eq!(FAILING_ENSURE_COUNT.load(Ordering::SeqCst), 1);
assert!(!store.ensure_surface(&tab, ProfileDataMode::Transient, &[]).changed);
store.flush_runtime_for_test();
let _ = store.tick(&[tab.id().clone()]);
assert_eq!(FAILING_ENSURE_COUNT.load(Ordering::SeqCst), 1);
assert_eq!(
store.record_viewport_size(tab.id(), resized_viewport_bounds(), 1.0),
WebSurfaceInputOutcome::Applied,
);
assert!(store.ensure_surface(&tab, ProfileDataMode::Transient, &[]).changed);
store.flush_runtime_for_test();
let _ = store.tick(&[tab.id().clone()]);
assert_eq!(FAILING_ENSURE_COUNT.load(Ordering::SeqCst), 2);
Ok(())
}
#[test]
fn session_scope_change_resets_tab_state() {
let tab_id = TabId::new();
@@ -101,28 +209,110 @@ fn session_scope_change_resets_tab_state() {
}
struct FakeLiveRuntimeClient;
struct IdleSkipLiveRuntimeClient;
struct SidecarExitLiveRuntimeClient;
struct FailingLiveRuntimeClient;
impl LiveRuntimeClient for FakeLiveRuntimeClient {
fn ensure(&mut self, _request: ServoLiveEnsureRequest) -> Result<Option<WebLiveFrame>, String> {
fn ensure(
&mut self,
_request: ServoLiveEnsureRequest,
) -> Result<Option<ServoLiveFrame>, LiveRuntimeClientError> {
FAKE_ENSURE_COUNT.fetch_add(1, Ordering::SeqCst);
Ok(None)
}
fn poll(&mut self, _tab_id: String) -> Result<Option<WebLiveFrame>, String> {
fn poll(&mut self, _tab_id: String) -> Result<Option<ServoLiveFrame>, LiveRuntimeClientError> {
Ok(None)
}
fn close(&mut self, _tab_id: String) -> Result<(), String> {
fn close(&mut self, _tab_id: String) -> Result<(), LiveRuntimeClientError> {
FAKE_CLOSE_COUNT.fetch_add(1, Ordering::SeqCst);
Ok(())
}
}
impl LiveRuntimeClient for IdleSkipLiveRuntimeClient {
fn ensure(
&mut self,
_request: ServoLiveEnsureRequest,
) -> Result<Option<ServoLiveFrame>, LiveRuntimeClientError> {
IDLE_SKIP_ENSURE_COUNT.fetch_add(1, Ordering::SeqCst);
Ok(None)
}
fn poll(&mut self, _tab_id: String) -> Result<Option<ServoLiveFrame>, LiveRuntimeClientError> {
Ok(None)
}
fn close(&mut self, _tab_id: String) -> Result<(), LiveRuntimeClientError> {
Ok(())
}
}
impl LiveRuntimeClient for SidecarExitLiveRuntimeClient {
fn ensure(
&mut self,
_request: ServoLiveEnsureRequest,
) -> Result<Option<ServoLiveFrame>, LiveRuntimeClientError> {
Err(LiveRuntimeClientError::SidecarExited)
}
fn poll(&mut self, _tab_id: String) -> Result<Option<ServoLiveFrame>, LiveRuntimeClientError> {
Err(LiveRuntimeClientError::SidecarExited)
}
fn close(&mut self, _tab_id: String) -> Result<(), LiveRuntimeClientError> {
Err(LiveRuntimeClientError::SidecarExited)
}
}
impl LiveRuntimeClient for FailingLiveRuntimeClient {
fn ensure(
&mut self,
_request: ServoLiveEnsureRequest,
) -> Result<Option<ServoLiveFrame>, LiveRuntimeClientError> {
FAILING_ENSURE_COUNT.fetch_add(1, Ordering::SeqCst);
Err(LiveRuntimeClientError::SidecarExited)
}
fn poll(&mut self, _tab_id: String) -> Result<Option<ServoLiveFrame>, LiveRuntimeClientError> {
Ok(None)
}
fn close(&mut self, _tab_id: String) -> Result<(), LiveRuntimeClientError> {
Ok(())
}
}
fn fake_client_factory(
_config_dir: std::path::PathBuf,
) -> Result<Box<dyn LiveRuntimeClient>, String> {
Ok(Box::new(FakeLiveRuntimeClient))
}
fn idle_skip_client_factory(
_config_dir: std::path::PathBuf,
) -> Result<Box<dyn LiveRuntimeClient>, String> {
Ok(Box::new(IdleSkipLiveRuntimeClient))
}
fn recovery_client_factory(
_config_dir: std::path::PathBuf,
) -> Result<Box<dyn LiveRuntimeClient>, String> {
let factory_call = RECOVERY_FACTORY_COUNT.fetch_add(1, Ordering::SeqCst);
if factory_call == 0 {
return Ok(Box::new(SidecarExitLiveRuntimeClient));
}
Ok(Box::new(FakeLiveRuntimeClient))
}
fn failing_client_factory(
_config_dir: std::path::PathBuf,
) -> Result<Box<dyn LiveRuntimeClient>, String> {
Ok(Box::new(FailingLiveRuntimeClient))
}
fn web_tab(tab_id: TabId, profile_id: ProfileId, url: &str) -> Result<BrowserTab, String> {
let url = UrlText::parse(url).map_err(|error| error.to_string())?;
Ok(BrowserTab::new(tab_id, SpaceId::new(), profile_id, "Web", url))
@@ -132,8 +322,23 @@ fn surface_size() -> WebSurfaceSize {
WebSurfaceSize { width: 640, height: 480, device_pixel_ratio_percent: 100 }
}
fn viewport_bounds() -> gpui::Bounds<gpui::Pixels> {
gpui::Bounds::new(
gpui::point(gpui::px(0.0), gpui::px(0.0)),
gpui::size(gpui::px(640.0), gpui::px(480.0)),
)
}
fn resized_viewport_bounds() -> gpui::Bounds<gpui::Pixels> {
gpui::Bounds::new(
gpui::point(gpui::px(0.0), gpui::px(0.0)),
gpui::size(gpui::px(720.0), gpui::px(480.0)),
)
}
fn pending_input() -> WebSurfacePendingInput {
WebSurfacePendingInput {
enqueued_at: None,
scroll_offset: WebSurfaceScrollOffset::default(),
scroll_delta: None,
scroll_point: None,