perf(app): skip unchanged software web frames
This commit is contained in:
@@ -1033,6 +1033,7 @@ fn identical_live_frames_share_render_image_arc() -> Result<(), String> {
|
||||
Arc::as_ptr(first_image),
|
||||
Arc::as_ptr(second_image),
|
||||
);
|
||||
assert!(first.has_same_software_render_as(&second));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -6,17 +6,17 @@ use gpui::{Bounds, Pixels, Point};
|
||||
|
||||
use crate::services::ProfileDataMode;
|
||||
|
||||
use super::web_surface_metadata::WebSurfacePageMetadata;
|
||||
use super::{
|
||||
web_surface_cadence::IDLE_POLL_INTERVAL,
|
||||
web_surface_frame::WebSurfaceFrame,
|
||||
web_surface_geometry::{WebSurfaceClickPoint, WebSurfaceScrollDelta, WebSurfaceSize},
|
||||
web_surface_metadata::WebSurfacePageMetadata,
|
||||
web_surface_permissions::WebSurfaceSitePermission,
|
||||
web_surface_runtime::{WebSurfaceRuntime, WebSurfaceRuntimeFrame, WebSurfaceUrlChange},
|
||||
web_surface_runtime::{WebSurfaceRuntime, WebSurfaceRuntimeFrame},
|
||||
web_surface_state::{
|
||||
PerTabSurface, WebSurfaceClickState, WebSurfaceEnsureKey, WebSurfaceInputOutcome,
|
||||
WebSurfaceKeyboardFocusState, WebSurfacePendingInput, WebSurfaceScrollState,
|
||||
WebSurfaceState, WebSurfaceTextInputState,
|
||||
WebSurfaceState, WebSurfaceTextInputState, WebSurfaceTickResult,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -118,14 +118,18 @@ impl WebSurfaceStore {
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if let Some(metadata) = WebSurfacePageMetadata::from_frame(&tab_id, &frame) {
|
||||
result.page_metadata.push(metadata);
|
||||
}
|
||||
self.surface_mut(&tab_id).state = Some(WebSurfaceState::Ready(*frame));
|
||||
result.changed = true;
|
||||
if let Some(url_change) = url_change {
|
||||
result.url_changes.push(url_change);
|
||||
result
|
||||
.page_metadata
|
||||
.extend(WebSurfacePageMetadata::from_frame(&tab_id, &frame));
|
||||
if self
|
||||
.surfaces
|
||||
.get(&tab_id)
|
||||
.is_none_or(|surface| !surface.matches_ready(&frame))
|
||||
{
|
||||
self.surface_mut(&tab_id).state = Some(WebSurfaceState::Ready(*frame));
|
||||
result.changed = true;
|
||||
}
|
||||
result.url_changes.extend(url_change);
|
||||
}
|
||||
WebSurfaceRuntimeFrame::Failed { tab_id, message } => {
|
||||
let had_ready = matches!(
|
||||
@@ -483,13 +487,6 @@ pub(super) fn is_external_web_url(url: &str) -> bool {
|
||||
url.starts_with("https://") || url.starts_with("http://")
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct WebSurfaceTickResult {
|
||||
pub(super) changed: bool,
|
||||
pub(super) url_changes: Vec<WebSurfaceUrlChange>,
|
||||
pub(super) page_metadata: Vec<WebSurfacePageMetadata>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "web_surface_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -14,24 +14,9 @@ use thiserror::Error;
|
||||
use crate::services::servo_live::ServoLiveFrame;
|
||||
|
||||
thread_local! {
|
||||
/// Single-slot cache of the most-recently-uploaded RGBA payload.
|
||||
/// At 60 fps on a 1080p canvas the previous `from_parts` was
|
||||
/// unconditionally building a fresh `Arc<RenderImage>` for every
|
||||
/// tick — even when the bytes were bit-identical to the last
|
||||
/// frame. The cache keys on a 64-bit hash of the raw bytes and
|
||||
/// reuses the existing `Arc<RenderImage>` whenever the hash
|
||||
/// matches, so steady-state idle pages no longer churn the GPUI
|
||||
/// texture pool.
|
||||
///
|
||||
/// Uses `AHasher` instead of std's `DefaultHasher`. SipHash13
|
||||
/// (default) tops out around ~1.5 GB/s; an 8 MB 1080p frame
|
||||
/// hashes in ~5 ms on a modern CPU, which eats roughly 30 % of
|
||||
/// the 16 ms scroll budget on every cache-miss frame. AHash
|
||||
/// runs ~10 GB/s on the same hardware, dropping the per-frame
|
||||
/// hash cost to ~0.8 ms and giving the scroll path back most of
|
||||
/// that budget. Hash collisions remain ~1 in 2^64; if they ever
|
||||
/// matter we'll trade in length + first/last 32 bytes as a
|
||||
/// disambiguator before paying the full memcmp.
|
||||
/// Single-slot cache for byte-identical software frames.
|
||||
/// AHash keeps the 1080p hash pass below the frame budget while
|
||||
/// avoiding repeated GPUI texture allocation for idle pages.
|
||||
static LAST_FRAME_IMAGE: RefCell<Option<(u64, Arc<RenderImage>)>> =
|
||||
const { RefCell::new(None) };
|
||||
}
|
||||
@@ -62,12 +47,9 @@ pub(super) struct WebSurfaceFrame {
|
||||
content_pixel_count: u64,
|
||||
#[cfg(all(test, feature = "live-site-smoke"))]
|
||||
sample_hash: u64,
|
||||
/// Software-path image built from RGBA bytes when the sidecar runs
|
||||
/// without hardware surface publication.
|
||||
/// Software-path image built from RGBA bytes.
|
||||
pub(super) image: Option<Arc<RenderImage>>,
|
||||
/// Hardware-path surface imported from the sidecar's IOSurface.
|
||||
/// GPUI is patched locally to present BGRA CVPixelBuffers through
|
||||
/// `surface(...)`, so hardware frames can skip the RGBA pipe.
|
||||
/// Hardware-path IOSurface imported from the sidecar.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub(super) pixel_buffer: Option<CVPixelBuffer>,
|
||||
}
|
||||
@@ -231,10 +213,33 @@ impl WebSurfaceFrame {
|
||||
self.title.as_deref()
|
||||
}
|
||||
|
||||
pub(super) fn has_same_software_render_as(&self, other: &Self) -> bool {
|
||||
#[cfg(target_os = "macos")]
|
||||
if self.pixel_buffer.is_some() || other.pixel_buffer.is_some() {
|
||||
return false;
|
||||
}
|
||||
let (Some(image), Some(other_image)) = (self.image.as_ref(), other.image.as_ref()) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
Arc::ptr_eq(image, other_image)
|
||||
&& self.requested_url == other.requested_url
|
||||
&& self.loaded_url == other.loaded_url
|
||||
&& self.title == other.title
|
||||
&& self.render_state == other.render_state
|
||||
&& self.width == other.width
|
||||
&& self.height == other.height
|
||||
&& self.device_pixel_ratio == other.device_pixel_ratio
|
||||
&& self.css_viewport_width == other.css_viewport_width
|
||||
&& self.css_viewport_height == other.css_viewport_height
|
||||
&& self.scroll_offset == other.scroll_offset
|
||||
&& self.zoom_percent == other.zoom_percent
|
||||
&& self.click_point == other.click_point
|
||||
&& self.typed_text == other.typed_text
|
||||
}
|
||||
|
||||
pub(super) fn has_visible_content_for_initial_display(&self) -> Result<bool, WebSurfaceError> {
|
||||
// The sidecar suppresses blank initial hardware frames with readback
|
||||
// sampling before sending them. Keep this app-side gate metadata-only
|
||||
// so GPUI's update path never locks or scans IOSurface memory.
|
||||
// Sidecar readback suppresses blank initial frames before publication.
|
||||
#[cfg(all(test, feature = "live-site-smoke"))]
|
||||
{
|
||||
Ok(self.non_white_pixel_count > 0 && self.content_pixel_count > 0)
|
||||
|
||||
@@ -26,6 +26,7 @@ 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);
|
||||
static REPEATED_FRAME_ENSURE_COUNT: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
#[test]
|
||||
fn runtime_keeps_independent_clients_for_profile_scopes() -> Result<(), String> {
|
||||
@@ -137,6 +138,48 @@ fn store_tick_delay_tracks_runtime_cadence() -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn identical_ready_software_frame_keeps_tick_unchanged() -> Result<(), String> {
|
||||
REPEATED_FRAME_ENSURE_COUNT.store(0, Ordering::SeqCst);
|
||||
let mut store = WebSurfaceStore::new_with_runtime(WebSurfaceRuntime::new_with_client_factory(
|
||||
repeated_frame_client_factory,
|
||||
));
|
||||
let tab = web_tab(TabId::new(), ProfileId::new(), "https://example.com/repeated")?;
|
||||
let visible_tabs = vec![tab.id().clone()];
|
||||
|
||||
assert_eq!(
|
||||
store.record_viewport_size(tab.id(), viewport_bounds(), 1.0),
|
||||
WebSurfaceInputOutcome::Applied,
|
||||
);
|
||||
assert!(store.ensure_surface(&tab, ProfileDataMode::Transient, &[]));
|
||||
store.flush_runtime_for_test();
|
||||
let first_tick = store.tick(&visible_tabs);
|
||||
|
||||
assert!(first_tick.changed);
|
||||
assert_eq!(first_tick.page_metadata.len(), 1);
|
||||
assert_eq!(first_tick.url_changes.len(), 1);
|
||||
assert_eq!(REPEATED_FRAME_ENSURE_COUNT.load(Ordering::SeqCst), 1);
|
||||
|
||||
assert_eq!(
|
||||
store.record_click_point(
|
||||
tab.id(),
|
||||
tab.url().as_str(),
|
||||
gpui::point(gpui::px(10.0), gpui::px(10.0)),
|
||||
1.0,
|
||||
),
|
||||
WebSurfaceInputOutcome::Applied,
|
||||
);
|
||||
let _ = store.ensure_surface(&tab, ProfileDataMode::Transient, &[]);
|
||||
store.flush_runtime_for_test();
|
||||
let second_tick = store.tick(&visible_tabs);
|
||||
|
||||
assert!(!second_tick.changed);
|
||||
assert_eq!(second_tick.page_metadata.len(), 1);
|
||||
assert_eq!(second_tick.url_changes.len(), 1);
|
||||
assert_eq!(REPEATED_FRAME_ENSURE_COUNT.load(Ordering::SeqCst), 2);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sidecar_exit_removes_dead_runtime_client() -> Result<(), String> {
|
||||
RECOVERY_FACTORY_COUNT.store(0, Ordering::SeqCst);
|
||||
@@ -238,6 +281,7 @@ struct FakeLiveRuntimeClient;
|
||||
struct IdleSkipLiveRuntimeClient;
|
||||
struct SidecarExitLiveRuntimeClient;
|
||||
struct FailingLiveRuntimeClient;
|
||||
struct RepeatedFrameLiveRuntimeClient;
|
||||
|
||||
impl LiveRuntimeClient for FakeLiveRuntimeClient {
|
||||
fn ensure(
|
||||
@@ -311,6 +355,24 @@ impl LiveRuntimeClient for FailingLiveRuntimeClient {
|
||||
}
|
||||
}
|
||||
|
||||
impl LiveRuntimeClient for RepeatedFrameLiveRuntimeClient {
|
||||
fn ensure(
|
||||
&mut self,
|
||||
_request: ServoLiveEnsureRequest,
|
||||
) -> Result<Option<ServoLiveFrame>, LiveRuntimeClientError> {
|
||||
REPEATED_FRAME_ENSURE_COUNT.fetch_add(1, Ordering::SeqCst);
|
||||
Ok(Some(repeated_live_frame()))
|
||||
}
|
||||
|
||||
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> {
|
||||
@@ -339,6 +401,16 @@ fn failing_client_factory(
|
||||
Ok(Box::new(FailingLiveRuntimeClient))
|
||||
}
|
||||
|
||||
fn repeated_frame_client_factory(
|
||||
_config_dir: std::path::PathBuf,
|
||||
) -> Result<Box<dyn LiveRuntimeClient>, String> {
|
||||
Ok(Box::new(RepeatedFrameLiveRuntimeClient))
|
||||
}
|
||||
|
||||
fn repeated_live_frame() -> ServoLiveFrame {
|
||||
ServoLiveFrame::for_test(1, 1, vec![16, 32, 64, 255])
|
||||
}
|
||||
|
||||
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))
|
||||
|
||||
@@ -9,7 +9,9 @@ use super::{
|
||||
web_surface_geometry::{
|
||||
WebSurfaceClickPoint, WebSurfaceScrollDelta, WebSurfaceScrollOffset, WebSurfaceSize,
|
||||
},
|
||||
web_surface_metadata::WebSurfacePageMetadata,
|
||||
web_surface_permissions::WebSurfaceSitePermission,
|
||||
web_surface_runtime::WebSurfaceUrlChange,
|
||||
};
|
||||
|
||||
pub(super) struct WebSurfaceScrollState {
|
||||
@@ -101,6 +103,13 @@ pub(super) enum WebSurfaceState {
|
||||
Failed { message: String },
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct WebSurfaceTickResult {
|
||||
pub(super) changed: bool,
|
||||
pub(super) url_changes: Vec<WebSurfaceUrlChange>,
|
||||
pub(super) page_metadata: Vec<WebSurfacePageMetadata>,
|
||||
}
|
||||
|
||||
/// All per-tab surface invariants in one owner.
|
||||
///
|
||||
/// Replaces the previous 11 parallel `BTreeMap<TabId, _>` fields on
|
||||
@@ -179,6 +188,13 @@ impl PerTabSurface {
|
||||
self.last_ensure_key = Some(key);
|
||||
}
|
||||
|
||||
pub(super) fn matches_ready(&self, frame: &WebSurfaceFrame) -> bool {
|
||||
match self.state.as_ref() {
|
||||
Some(WebSurfaceState::Ready(current)) => current.has_same_software_render_as(frame),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn has_pending_input(&self) -> bool {
|
||||
self.hover_point.is_some()
|
||||
|| self.click_point.is_some()
|
||||
|
||||
Reference in New Issue
Block a user