fix: coalesce queued scroll input
This commit is contained in:
@@ -88,6 +88,10 @@ Real and verified:
|
|||||||
(LOAD 1 → LOAD 2). The webview loading state reconciles through
|
(LOAD 1 → LOAD 2). The webview loading state reconciles through
|
||||||
redirects/pushState (`awaiting_url_change`), and `run_dev.sh` resolves the
|
redirects/pushState (`awaiting_url_change`), and `run_dev.sh` resolves the
|
||||||
macOS Metal toolchain so `cargo run`'s shader-build failure is fixed.
|
macOS Metal toolchain so `cargo run`'s shader-build failure is fixed.
|
||||||
|
- Scroll input: consecutive same-document wheel requests coalesce at the
|
||||||
|
profile worker boundary while preserving total device-pixel distance;
|
||||||
|
clicks, text, and allow-once permission transfers retain strict ordering.
|
||||||
|
Verified by a blocked-worker burst regression and the live-site scroll e2e.
|
||||||
- Cross-platform: download Open/Reveal use per-OS launchers (macOS `open`,
|
- Cross-platform: download Open/Reveal use per-OS launchers (macOS `open`,
|
||||||
Windows `cmd start`/`explorer /select`, Linux `xdg-open`); the command
|
Windows `cmd start`/`explorer /select`, Linux `xdg-open`); the command
|
||||||
overlay closes on Escape.
|
overlay closes on Escape.
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ use crate::services::servo_live::{
|
|||||||
#[path = "web_surface_worker_dispatch.rs"]
|
#[path = "web_surface_worker_dispatch.rs"]
|
||||||
mod dispatch;
|
mod dispatch;
|
||||||
use dispatch::{
|
use dispatch::{
|
||||||
dispatch_result, forward_permission_consumptions, preserve_latest_hover,
|
can_merge_consecutive_scroll, dispatch_result, forward_permission_consumptions,
|
||||||
request_has_ordered_input,
|
merge_consecutive_scroll, preserve_latest_hover, request_has_ordered_input,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Blocking transport for one profile-scoped Servo sidecar.
|
/// Blocking transport for one profile-scoped Servo sidecar.
|
||||||
@@ -225,11 +225,12 @@ impl LiveRuntimeWorker {
|
|||||||
}
|
}
|
||||||
let mut request = WorkerRequest::Ensure { generation, request: Box::new(request) };
|
let mut request = WorkerRequest::Ensure { generation, request: Box::new(request) };
|
||||||
if let Some(pending) = q.pending.get_mut(&tab_id) {
|
if let Some(pending) = q.pending.get_mut(&tab_id) {
|
||||||
let replace_tail = pending.back().is_some_and(|tail| {
|
if let Some(tail) = pending.back_mut()
|
||||||
matches!(tail, WorkerRequest::Poll { .. })
|
&& (can_merge_consecutive_scroll(&request, tail)
|
||||||
|| (!request_has_ordered_input(&request) && !request_has_ordered_input(tail))
|
|| matches!(tail, WorkerRequest::Poll { .. })
|
||||||
});
|
|| (!request_has_ordered_input(&request) && !request_has_ordered_input(tail)))
|
||||||
if replace_tail && let Some(tail) = pending.back_mut() {
|
{
|
||||||
|
merge_consecutive_scroll(&mut request, tail);
|
||||||
preserve_latest_hover(&mut request, tail);
|
preserve_latest_hover(&mut request, tail);
|
||||||
*tail = request;
|
*tail = request;
|
||||||
} else {
|
} else {
|
||||||
@@ -241,7 +242,6 @@ impl LiveRuntimeWorker {
|
|||||||
}
|
}
|
||||||
cvar.notify_one();
|
cvar.notify_one();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn submit_poll(&self, generation: RequestGeneration, tab_id: String) -> bool {
|
pub(super) fn submit_poll(&self, generation: RequestGeneration, tab_id: String) -> bool {
|
||||||
let (lock, cvar) = &*self.queue;
|
let (lock, cvar) = &*self.queue;
|
||||||
let mut q = match lock.lock() {
|
let mut q = match lock.lock() {
|
||||||
|
|||||||
@@ -6,6 +6,55 @@ use super::{
|
|||||||
LiveRuntimeClient, LiveRuntimeClientError, RequestGeneration, WorkerRequest, WorkerResponse,
|
LiveRuntimeClient, LiveRuntimeClientError, RequestGeneration, WorkerRequest, WorkerResponse,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub(super) fn can_merge_consecutive_scroll(
|
||||||
|
latest: &WorkerRequest,
|
||||||
|
previous: &WorkerRequest,
|
||||||
|
) -> bool {
|
||||||
|
let (
|
||||||
|
WorkerRequest::Ensure { request: latest, .. },
|
||||||
|
WorkerRequest::Ensure { request: previous, .. },
|
||||||
|
) = (latest, previous)
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
is_scroll_only(latest)
|
||||||
|
&& is_scroll_only(previous)
|
||||||
|
&& latest.profile_id == previous.profile_id
|
||||||
|
&& latest.url == previous.url
|
||||||
|
&& latest.width == previous.width
|
||||||
|
&& latest.height == previous.height
|
||||||
|
&& latest.page_zoom_percent == previous.page_zoom_percent
|
||||||
|
&& latest.device_pixel_ratio == previous.device_pixel_ratio
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn merge_consecutive_scroll(latest: &mut WorkerRequest, previous: &WorkerRequest) {
|
||||||
|
if !can_merge_consecutive_scroll(latest, previous) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let (
|
||||||
|
WorkerRequest::Ensure { request: latest, .. },
|
||||||
|
WorkerRequest::Ensure { request: previous, .. },
|
||||||
|
) = (latest, previous)
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
latest.scroll_delta_x = previous.scroll_delta_x.saturating_add(latest.scroll_delta_x);
|
||||||
|
latest.scroll_delta_y = previous.scroll_delta_y.saturating_add(latest.scroll_delta_y);
|
||||||
|
if latest.hover_x.is_none() && latest.hover_y.is_none() {
|
||||||
|
latest.hover_x = previous.hover_x;
|
||||||
|
latest.hover_y = previous.hover_y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_scroll_only(request: &crate::services::servo_live::ServoLiveEnsureRequest) -> bool {
|
||||||
|
(request.scroll_delta_x != 0 || request.scroll_delta_y != 0)
|
||||||
|
&& request.click_x.is_none()
|
||||||
|
&& request.click_y.is_none()
|
||||||
|
&& request.typed_text.is_none()
|
||||||
|
&& !request.site_permissions.iter().any(|permission| permission.state == "allow-once")
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn forward_permission_consumptions(
|
pub(super) fn forward_permission_consumptions(
|
||||||
client: &mut dyn LiveRuntimeClient,
|
client: &mut dyn LiveRuntimeClient,
|
||||||
response_tx: &mpsc::Sender<WorkerResponse>,
|
response_tx: &mpsc::Sender<WorkerResponse>,
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
use std::{
|
||||||
|
sync::{Arc, Mutex, mpsc},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::services::servo_live::{ServoLiveEnsureRequest, ServoLiveFrame};
|
||||||
|
|
||||||
|
use super::super::{
|
||||||
|
LiveRuntimeClient, LiveRuntimeClientError, LiveRuntimeWorker, RequestGeneration,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct BlockingScrollClient {
|
||||||
|
deltas: Arc<Mutex<Vec<(i32, i32)>>>,
|
||||||
|
first_started_tx: Option<mpsc::Sender<()>>,
|
||||||
|
release_first_rx: mpsc::Receiver<()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LiveRuntimeClient for BlockingScrollClient {
|
||||||
|
fn ensure(
|
||||||
|
&mut self,
|
||||||
|
request: ServoLiveEnsureRequest,
|
||||||
|
) -> Result<Option<ServoLiveFrame>, LiveRuntimeClientError> {
|
||||||
|
self.deltas
|
||||||
|
.lock()
|
||||||
|
.map_err(|_| "scroll recorder lock was poisoned".to_string())?
|
||||||
|
.push((request.scroll_delta_x, request.scroll_delta_y));
|
||||||
|
if let Some(first_started_tx) = self.first_started_tx.take() {
|
||||||
|
first_started_tx.send(()).map_err(|error| error.to_string())?;
|
||||||
|
self.release_first_rx.recv().map_err(|error| error.to_string())?;
|
||||||
|
}
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn consecutive_scroll_updates_coalesce_without_losing_distance() -> Result<(), String> {
|
||||||
|
let deltas = Arc::new(Mutex::new(Vec::new()));
|
||||||
|
let client_deltas = deltas.clone();
|
||||||
|
let (first_started_tx, first_started_rx) = mpsc::channel();
|
||||||
|
let (release_first_tx, release_first_rx) = mpsc::channel();
|
||||||
|
let worker = LiveRuntimeWorker::new(move || {
|
||||||
|
Ok(Box::new(BlockingScrollClient {
|
||||||
|
deltas: client_deltas,
|
||||||
|
first_started_tx: Some(first_started_tx),
|
||||||
|
release_first_rx,
|
||||||
|
}))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
worker.submit_ensure(RequestGeneration::new(1), ensure_request(0));
|
||||||
|
first_started_rx.recv_timeout(Duration::from_secs(1)).map_err(|error| error.to_string())?;
|
||||||
|
for generation in 2..=101 {
|
||||||
|
worker.submit_ensure(RequestGeneration::new(generation), ensure_request(1));
|
||||||
|
}
|
||||||
|
release_first_tx.send(()).map_err(|error| error.to_string())?;
|
||||||
|
worker.wait_until_idle();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
*deltas.lock().map_err(|_| "scroll recorder lock was poisoned".to_string())?,
|
||||||
|
vec![(0, 0), (100, 100)]
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_request(scroll_delta: i32) -> ServoLiveEnsureRequest {
|
||||||
|
ServoLiveEnsureRequest {
|
||||||
|
tab_id: "tab-a".to_string(),
|
||||||
|
profile_id: "profile".to_string(),
|
||||||
|
url: "https://example.com/".to_string(),
|
||||||
|
width: 640,
|
||||||
|
height: 480,
|
||||||
|
page_zoom_percent: 100,
|
||||||
|
device_pixel_ratio: 1.0,
|
||||||
|
scroll_delta_x: scroll_delta,
|
||||||
|
scroll_delta_y: scroll_delta,
|
||||||
|
scroll_point_x: (scroll_delta != 0).then_some(1),
|
||||||
|
scroll_point_y: (scroll_delta != 0).then_some(1),
|
||||||
|
click_x: None,
|
||||||
|
click_y: None,
|
||||||
|
hover_x: None,
|
||||||
|
hover_y: None,
|
||||||
|
typed_text: None,
|
||||||
|
site_permission_generation: 1,
|
||||||
|
site_permissions: Vec::new(),
|
||||||
|
allow_once_grants: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -464,3 +464,6 @@ fn ensure_request(tab_id: &str, input: RecordedInput) -> ServoLiveEnsureRequest
|
|||||||
allow_once_grants: Vec::new(),
|
allow_once_grants: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[path = "web_surface_worker_scroll_tests.rs"]
|
||||||
|
mod scroll_tests;
|
||||||
|
|||||||
Reference in New Issue
Block a user