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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user