mirror of
https://github.com/servo/servo.git
synced 2025-08-09 07:25:35 +01:00
script: Unify script-based "update the rendering" and throttle it to 60 FPS
Signed-off-by: Martin Robinson <mrobinson@igalia.com>
This commit is contained in:
parent
c09e117bfe
commit
3640c027f2
4 changed files with 131 additions and 123 deletions
|
@ -3658,6 +3658,29 @@ impl Document {
|
||||||
self.webgpu_contexts.clone()
|
self.webgpu_contexts.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether or not this [`Document`] needs a rendering update, due to changed
|
||||||
|
/// contents or pending events.
|
||||||
|
pub(crate) fn needs_rendering_update(&self) -> bool {
|
||||||
|
if !self.is_fully_active() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if !self.window().layout_blocked() && !self.restyle_reason().is_empty() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if self.has_pending_input_events() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if self.has_resize_observers() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.window().has_unhandled_resize_event() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
/// An implementation of step 22 from
|
/// An implementation of step 22 from
|
||||||
/// <https://html.spec.whatwg.org/multipage/#update-the-rendering>:
|
/// <https://html.spec.whatwg.org/multipage/#update-the-rendering>:
|
||||||
///
|
///
|
||||||
|
@ -3718,6 +3741,11 @@ impl Document {
|
||||||
.push(Dom::from_ref(resize_observer));
|
.push(Dom::from_ref(resize_observer));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether or not this [`Document`] has any active [`ResizeObserver`].
|
||||||
|
pub(crate) fn has_resize_observers(&self) -> bool {
|
||||||
|
!self.resize_observers.borrow().is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
/// <https://drafts.csswg.org/resize-observer/#gather-active-observations-h>
|
/// <https://drafts.csswg.org/resize-observer/#gather-active-observations-h>
|
||||||
/// <https://drafts.csswg.org/resize-observer/#has-active-resize-observations>
|
/// <https://drafts.csswg.org/resize-observer/#has-active-resize-observations>
|
||||||
pub(crate) fn gather_active_resize_observations_at_depth(
|
pub(crate) fn gather_active_resize_observations_at_depth(
|
||||||
|
@ -4429,6 +4457,12 @@ impl Document {
|
||||||
mem::take(&mut *self.pending_input_events.borrow_mut())
|
mem::take(&mut *self.pending_input_events.borrow_mut())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether or not this [`Document`] has any pending input events to be processed during
|
||||||
|
/// "update the rendering."
|
||||||
|
pub(crate) fn has_pending_input_events(&self) -> bool {
|
||||||
|
!self.pending_input_events.borrow().is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn set_csp_list(&self, csp_list: Option<CspList>) {
|
pub(crate) fn set_csp_list(&self, csp_list: Option<CspList>) {
|
||||||
self.policy_container.borrow_mut().set_csp_list(csp_list);
|
self.policy_container.borrow_mut().set_csp_list(csp_list);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2776,6 +2776,11 @@ impl Window {
|
||||||
self.unhandled_resize_event.borrow_mut().take()
|
self.unhandled_resize_event.borrow_mut().take()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether or not this [`Window`] has any resize events that have not been processed.
|
||||||
|
pub(crate) fn has_unhandled_resize_event(&self) -> bool {
|
||||||
|
self.unhandled_resize_event.borrow().is_some()
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn set_viewport_size(&self, new_viewport_size: UntypedSize2D<f32>) {
|
pub(crate) fn set_viewport_size(&self, new_viewport_size: UntypedSize2D<f32>) {
|
||||||
let new_viewport_size = Size2D::new(
|
let new_viewport_size = Size2D::new(
|
||||||
Au::from_f32_px(new_viewport_size.width),
|
Au::from_f32_px(new_viewport_size.width),
|
||||||
|
|
|
@ -200,7 +200,8 @@ type NodeIdSet = HashSet<String>;
|
||||||
#[cfg_attr(crown, allow(crown::unrooted_must_root))]
|
#[cfg_attr(crown, allow(crown::unrooted_must_root))]
|
||||||
pub struct ScriptThread {
|
pub struct ScriptThread {
|
||||||
/// <https://html.spec.whatwg.org/multipage/#last-render-opportunity-time>
|
/// <https://html.spec.whatwg.org/multipage/#last-render-opportunity-time>
|
||||||
last_render_opportunity_time: DomRefCell<Option<Instant>>,
|
last_render_opportunity_time: Cell<Option<Instant>>,
|
||||||
|
|
||||||
/// The documents for pipelines managed by this thread
|
/// The documents for pipelines managed by this thread
|
||||||
documents: DomRefCell<DocumentCollection>,
|
documents: DomRefCell<DocumentCollection>,
|
||||||
/// The window proxies known by this thread
|
/// The window proxies known by this thread
|
||||||
|
@ -1208,20 +1209,7 @@ impl ScriptThread {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <https://html.spec.whatwg.org/multipage/#update-the-rendering>
|
fn cancel_scheduled_update_the_rendering(&self) {
|
||||||
///
|
|
||||||
/// Attempt to update the rendering and then do a microtask checkpoint if rendering was actually
|
|
||||||
/// updated.
|
|
||||||
pub(crate) fn update_the_rendering(&self, can_gc: CanGc) {
|
|
||||||
*self.last_render_opportunity_time.borrow_mut() = Some(Instant::now());
|
|
||||||
|
|
||||||
let is_animation_tick = self.has_pending_animation_tick.load(Ordering::Relaxed);
|
|
||||||
if is_animation_tick {
|
|
||||||
self.has_pending_animation_tick
|
|
||||||
.store(false, Ordering::Relaxed);
|
|
||||||
// If this is an animation tick, cancel any upcoming ScriptThread-based animation timer.
|
|
||||||
// This tick serves the purpose and we to limit animation ticks if some are coming from
|
|
||||||
// the renderer.
|
|
||||||
if let Some(timer_id) = self
|
if let Some(timer_id) = self
|
||||||
.scheduled_script_thread_animation_timer
|
.scheduled_script_thread_animation_timer
|
||||||
.borrow_mut()
|
.borrow_mut()
|
||||||
|
@ -1231,14 +1219,39 @@ impl ScriptThread {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !self.can_continue_running_inner() {
|
fn schedule_update_the_rendering_timer_if_necessary(&self, delay: Duration) {
|
||||||
|
if self
|
||||||
|
.scheduled_script_thread_animation_timer
|
||||||
|
.borrow()
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let any_animations_running = self.documents.borrow().iter().any(|(_, document)| {
|
debug!("Scheduling ScriptThread animation frame.");
|
||||||
document.is_fully_active() && document.animations().running_animation_count() != 0
|
let trigger_script_thread_animation = self.has_pending_animation_tick.clone();
|
||||||
|
let timer_id = self.schedule_timer(TimerEventRequest {
|
||||||
|
callback: Box::new(move || {
|
||||||
|
trigger_script_thread_animation.store(true, Ordering::Relaxed);
|
||||||
|
}),
|
||||||
|
duration: delay,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
*self.scheduled_script_thread_animation_timer.borrow_mut() = Some(timer_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <https://html.spec.whatwg.org/multipage/#update-the-rendering>
|
||||||
|
///
|
||||||
|
/// Attempt to update the rendering and then do a microtask checkpoint if rendering was actually
|
||||||
|
/// updated.
|
||||||
|
pub(crate) fn update_the_rendering(&self, can_gc: CanGc) {
|
||||||
|
self.last_render_opportunity_time.set(Some(Instant::now()));
|
||||||
|
self.cancel_scheduled_update_the_rendering();
|
||||||
|
|
||||||
|
if !self.can_continue_running_inner() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: The specification says to filter out non-renderable documents,
|
// TODO: The specification says to filter out non-renderable documents,
|
||||||
// as well as those for which a rendering update would be unnecessary,
|
// as well as those for which a rendering update would be unnecessary,
|
||||||
// but this isn't happening here.
|
// but this isn't happening here.
|
||||||
|
@ -1247,13 +1260,6 @@ impl ScriptThread {
|
||||||
// has pending initial observation targets
|
// has pending initial observation targets
|
||||||
// https://w3c.github.io/IntersectionObserver/#pending-initial-observation
|
// https://w3c.github.io/IntersectionObserver/#pending-initial-observation
|
||||||
|
|
||||||
// If we aren't explicitly running rAFs, this update wasn't requested by the compositor,
|
|
||||||
// and we are running animations, then wait until the compositor tells us it is time to
|
|
||||||
// update the rendering via a TickAllAnimations message.
|
|
||||||
if !is_animation_tick && any_animations_running {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// > 2. Let docs be all fully active Document objects whose relevant agent's event loop
|
// > 2. Let docs be all fully active Document objects whose relevant agent's event loop
|
||||||
// > is eventLoop, sorted arbitrarily except that the following conditions must be
|
// > is eventLoop, sorted arbitrarily except that the following conditions must be
|
||||||
// > met:
|
// > met:
|
||||||
|
@ -1323,9 +1329,7 @@ impl ScriptThread {
|
||||||
// > 14. For each doc of docs, run the animation frame callbacks for doc, passing
|
// > 14. For each doc of docs, run the animation frame callbacks for doc, passing
|
||||||
// > in the relative high resolution time given frameTimestamp and doc's
|
// > in the relative high resolution time given frameTimestamp and doc's
|
||||||
// > relevant global object as the timestamp.
|
// > relevant global object as the timestamp.
|
||||||
if is_animation_tick {
|
|
||||||
document.run_the_animation_frame_callbacks(can_gc);
|
document.run_the_animation_frame_callbacks(can_gc);
|
||||||
}
|
|
||||||
|
|
||||||
// Run the resize observer steps.
|
// Run the resize observer steps.
|
||||||
let _realm = enter_realm(&*document);
|
let _realm = enter_realm(&*document);
|
||||||
|
@ -1365,106 +1369,75 @@ impl ScriptThread {
|
||||||
// should be run in a task and a microtask checkpoint is always done when running tasks.
|
// should be run in a task and a microtask checkpoint is always done when running tasks.
|
||||||
self.perform_a_microtask_checkpoint(can_gc);
|
self.perform_a_microtask_checkpoint(can_gc);
|
||||||
|
|
||||||
// If there are pending reflows, they were probably caused by the execution of
|
self.maybe_schedule_rendering_opportunity_after_rendering_upate(saw_any_reflows);
|
||||||
// the microtask checkpoint above and we should spin the event loop one more
|
|
||||||
// time to resolve them.
|
|
||||||
self.schedule_rendering_opportunity_if_necessary();
|
|
||||||
|
|
||||||
// If this was a animation update request, then potentially schedule a new
|
|
||||||
// animation update in the case that the compositor might not do it due to
|
|
||||||
// not receiving any display lists.
|
|
||||||
if is_animation_tick {
|
|
||||||
self.schedule_script_thread_animation_tick_if_necessary(saw_any_reflows);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn maybe_schedule_rendering_opportunity_after_rendering_upate(&self, saw_any_reflows: bool) {
|
||||||
// If there are any pending reflows and we are not having rendering opportunities
|
// If there are any pending reflows and we are not having rendering opportunities
|
||||||
// driven by the compositor, then schedule the next rendering opportunity.
|
// driven by the compositor, then schedule the next rendering opportunity.
|
||||||
//
|
//
|
||||||
// TODO: This is a workaround until rendering opportunities can be triggered from a
|
// TODO: This is a workaround until rendering opportunities can be triggered from a
|
||||||
// timer in the script thread.
|
// timer in the script thread.
|
||||||
fn schedule_rendering_opportunity_if_necessary(&self) {
|
if self
|
||||||
// If any Document has active animations of rAFs, then we should be receiving
|
.documents
|
||||||
// regular rendering opportunities from the compositor (or fake animation frame
|
.borrow()
|
||||||
// ticks). In this case, don't schedule an opportunity, just wait for the next
|
.iter()
|
||||||
// one.
|
.any(|(_, document)| document.needs_rendering_update())
|
||||||
if self.documents.borrow().iter().any(|(_, document)| {
|
{
|
||||||
document.is_fully_active() &&
|
self.cancel_scheduled_update_the_rendering();
|
||||||
(document.animations().running_animation_count() != 0 ||
|
self.schedule_update_the_rendering_timer_if_necessary(Duration::ZERO);
|
||||||
document.has_active_request_animation_frame_callbacks())
|
|
||||||
}) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some((_, document)) = self.documents.borrow().iter().find(|(_, document)| {
|
if !saw_any_reflows &&
|
||||||
document.is_fully_active() &&
|
self.documents.borrow().iter().any(|(_, document)| {
|
||||||
!document.window().layout_blocked() &&
|
|
||||||
!document.restyle_reason().is_empty()
|
|
||||||
}) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Queues a task to update the rendering.
|
|
||||||
// <https://html.spec.whatwg.org/multipage/#event-loop-processing-model:queue-a-global-task>
|
|
||||||
//
|
|
||||||
// Note: The specification says to queue a task using the navigable's active
|
|
||||||
// window, but then updates the rendering for all documents.
|
|
||||||
//
|
|
||||||
// This task is empty because any new IPC messages in the ScriptThread trigger a
|
|
||||||
// rendering update when animations are not running.
|
|
||||||
let _realm = enter_realm(&*document);
|
|
||||||
document
|
|
||||||
.owner_global()
|
|
||||||
.task_manager()
|
|
||||||
.rendering_task_source()
|
|
||||||
.queue_unconditionally(task!(update_the_rendering: move || { }));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The renderer triggers animation ticks based on the arrival and painting of new
|
|
||||||
/// display lists. In the case that a `WebView` is animating or has a
|
|
||||||
/// requestAnimationFrame callback, it may be that an animation tick reflow does
|
|
||||||
/// not change anything and thus does not send a new display list to the renderer.
|
|
||||||
/// If that's the case, we need to schedule a ScriptThread-based animation update
|
|
||||||
/// (to avoid waking the renderer up).
|
|
||||||
fn schedule_script_thread_animation_tick_if_necessary(&self, saw_any_reflows: bool) {
|
|
||||||
if saw_any_reflows {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always schedule a ScriptThread-based animation tick, unless none of the
|
|
||||||
// documents are active and have animations running and/or rAF callbacks.
|
|
||||||
if !self.documents.borrow().iter().any(|(_, document)| {
|
|
||||||
document.is_fully_active() &&
|
document.is_fully_active() &&
|
||||||
!document.window().throttled() &&
|
!document.window().throttled() &&
|
||||||
(document.animations().running_animation_count() != 0 ||
|
(document.animations().running_animation_count() != 0 ||
|
||||||
document.has_active_request_animation_frame_callbacks())
|
document.has_active_request_animation_frame_callbacks())
|
||||||
}) {
|
})
|
||||||
|
{
|
||||||
|
const SCRIPT_THREAD_ANIMATION_TICK_DELAY: Duration = Duration::from_millis(30);
|
||||||
|
self.schedule_update_the_rendering_timer_if_necessary(
|
||||||
|
SCRIPT_THREAD_ANIMATION_TICK_DELAY,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn maybe_schedule_rendering_opportunity_after_ipc_message(&self, can_gc: CanGc) {
|
||||||
|
if self.has_pending_animation_tick.load(Ordering::Relaxed) {
|
||||||
|
self.update_the_rendering(can_gc);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The amount of time between ScriptThread animation ticks when nothing is
|
// If no document needs a rendering update, exit early to avoid doing more work.
|
||||||
/// changing. In order to be more efficient, only tick at around 30 frames a
|
if !self
|
||||||
/// second, which also gives time for any renderer ticks to come in and cancel
|
.documents
|
||||||
/// this tick. A renderer tick might happen for a variety of reasons, such as a
|
.borrow()
|
||||||
/// Pipeline in another ScriptThread producing a display list.
|
.iter()
|
||||||
const SCRIPT_THREAD_ANIMATION_TICK_DELAY: u64 = 30;
|
.any(|(_, document)| document.needs_rendering_update())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
debug!("Scheduling ScriptThread animation frame.");
|
// Wait 20 milliseconds between frames triggered by the script thread itself. This
|
||||||
let trigger_script_thread_animation = self.has_pending_animation_tick.clone();
|
// should, in theory, allow compositor-based ticks to arrive sooner.
|
||||||
let timer_id = self.schedule_timer(TimerEventRequest {
|
const SCRIPT_THREAD_ANIMATION_TICK_DELAY: Duration = Duration::from_millis(20);
|
||||||
callback: Box::new(move || {
|
let time_since_last_rendering_opportunity = self
|
||||||
trigger_script_thread_animation.store(true, Ordering::Relaxed);
|
.last_render_opportunity_time
|
||||||
}),
|
.get()
|
||||||
duration: Duration::from_millis(SCRIPT_THREAD_ANIMATION_TICK_DELAY),
|
.map(|last_render_opportunity_time| Instant::now() - last_render_opportunity_time)
|
||||||
});
|
.unwrap_or(Duration::MAX);
|
||||||
|
|
||||||
let mut scheduled_script_thread_animation_timer =
|
// If it's been more than the time of a single frame the last rendering opportunity,
|
||||||
self.scheduled_script_thread_animation_timer.borrow_mut();
|
// just run it now.
|
||||||
assert!(
|
if time_since_last_rendering_opportunity > SCRIPT_THREAD_ANIMATION_TICK_DELAY {
|
||||||
scheduled_script_thread_animation_timer.is_none(),
|
self.update_the_rendering(can_gc);
|
||||||
"Should never schedule a new timer when one is already scheduled."
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.schedule_update_the_rendering_timer_if_necessary(
|
||||||
|
SCRIPT_THREAD_ANIMATION_TICK_DELAY - time_since_last_rendering_opportunity,
|
||||||
);
|
);
|
||||||
*scheduled_script_thread_animation_timer = Some(timer_id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle incoming messages from other tasks and the task queue.
|
/// Handle incoming messages from other tasks and the task queue.
|
||||||
|
@ -1676,10 +1649,7 @@ impl ScriptThread {
|
||||||
docs.clear();
|
docs.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the rendering whenever we receive an IPC message. This may not actually do anything if
|
self.maybe_schedule_rendering_opportunity_after_ipc_message(can_gc);
|
||||||
// we are running animations and the compositor hasn't requested a new frame yet via a TickAllAnimatons
|
|
||||||
// message.
|
|
||||||
self.update_the_rendering(can_gc);
|
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
|
@ -145,7 +145,6 @@ impl TaskManager {
|
||||||
task_source_functions!(self, performance_timeline_task_source, PerformanceTimeline);
|
task_source_functions!(self, performance_timeline_task_source, PerformanceTimeline);
|
||||||
task_source_functions!(self, port_message_queue, PortMessage);
|
task_source_functions!(self, port_message_queue, PortMessage);
|
||||||
task_source_functions!(self, remote_event_task_source, RemoteEvent);
|
task_source_functions!(self, remote_event_task_source, RemoteEvent);
|
||||||
task_source_functions!(self, rendering_task_source, Rendering);
|
|
||||||
task_source_functions!(self, timer_task_source, Timer);
|
task_source_functions!(self, timer_task_source, Timer);
|
||||||
task_source_functions!(self, user_interaction_task_source, UserInteraction);
|
task_source_functions!(self, user_interaction_task_source, UserInteraction);
|
||||||
task_source_functions!(self, websocket_task_source, WebSocket);
|
task_source_functions!(self, websocket_task_source, WebSocket);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue