diff --git a/components/script/dom/document.rs b/components/script/dom/document.rs index c055240fecc..ea57e7a3b41 100644 --- a/components/script/dom/document.rs +++ b/components/script/dom/document.rs @@ -3658,6 +3658,29 @@ impl Document { 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 /// : /// @@ -3718,6 +3741,11 @@ impl Document { .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() + } + /// /// pub(crate) fn gather_active_resize_observations_at_depth( @@ -4429,6 +4457,12 @@ impl Document { 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) { self.policy_container.borrow_mut().set_csp_list(csp_list); } diff --git a/components/script/dom/window.rs b/components/script/dom/window.rs index e60a5e272b8..8ed3f9d33dc 100644 --- a/components/script/dom/window.rs +++ b/components/script/dom/window.rs @@ -2776,6 +2776,11 @@ impl Window { 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) { let new_viewport_size = Size2D::new( Au::from_f32_px(new_viewport_size.width), diff --git a/components/script/script_thread.rs b/components/script/script_thread.rs index f291b23c89d..fa68fcfe926 100644 --- a/components/script/script_thread.rs +++ b/components/script/script_thread.rs @@ -200,7 +200,8 @@ type NodeIdSet = HashSet; #[cfg_attr(crown, allow(crown::unrooted_must_root))] pub struct ScriptThread { /// - last_render_opportunity_time: DomRefCell>, + last_render_opportunity_time: Cell>, + /// The documents for pipelines managed by this thread documents: DomRefCell, /// The window proxies known by this thread @@ -1208,37 +1209,49 @@ impl ScriptThread { })); } + fn cancel_scheduled_update_the_rendering(&self) { + if let Some(timer_id) = self + .scheduled_script_thread_animation_timer + .borrow_mut() + .take() + { + self.timer_scheduler.borrow_mut().cancel_timer(timer_id); + } + } + + fn schedule_update_the_rendering_timer_if_necessary(&self, delay: Duration) { + if self + .scheduled_script_thread_animation_timer + .borrow() + .is_some() + { + return; + } + + debug!("Scheduling ScriptThread animation frame."); + 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); + } + /// /// /// 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 - .scheduled_script_thread_animation_timer - .borrow_mut() - .take() - { - self.timer_scheduler.borrow_mut().cancel_timer(timer_id); - } - } + self.last_render_opportunity_time.set(Some(Instant::now())); + self.cancel_scheduled_update_the_rendering(); if !self.can_continue_running_inner() { return; } - let any_animations_running = self.documents.borrow().iter().any(|(_, document)| { - document.is_fully_active() && document.animations().running_animation_count() != 0 - }); - // TODO: The specification says to filter out non-renderable documents, // as well as those for which a rendering update would be unnecessary, // but this isn't happening here. @@ -1247,13 +1260,6 @@ impl ScriptThread { // has pending initial observation targets // 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 // > is eventLoop, sorted arbitrarily except that the following conditions must be // > met: @@ -1323,9 +1329,7 @@ impl ScriptThread { // > 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 // > 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. 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. self.perform_a_microtask_checkpoint(can_gc); - // If there are pending reflows, they were probably caused by the execution of - // the microtask checkpoint above and we should spin the event loop one more - // time to resolve them. - self.schedule_rendering_opportunity_if_necessary(); + self.maybe_schedule_rendering_opportunity_after_rendering_upate(saw_any_reflows); + } - // 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 + // driven by the compositor, then schedule the next rendering opportunity. + // + // TODO: This is a workaround until rendering opportunities can be triggered from a + // timer in the script thread. + if self + .documents + .borrow() + .iter() + .any(|(_, document)| document.needs_rendering_update()) + { + self.cancel_scheduled_update_the_rendering(); + self.schedule_update_the_rendering_timer_if_necessary(Duration::ZERO); + } + + if !saw_any_reflows && + self.documents.borrow().iter().any(|(_, document)| { + document.is_fully_active() && + !document.window().throttled() && + (document.animations().running_animation_count() != 0 || + 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, + ); } } - // If there are any pending reflows and we are not having rendering opportunities - // driven by the compositor, then schedule the next rendering opportunity. - // - // TODO: This is a workaround until rendering opportunities can be triggered from a - // timer in the script thread. - fn schedule_rendering_opportunity_if_necessary(&self) { - // If any Document has active animations of rAFs, then we should be receiving - // regular rendering opportunities from the compositor (or fake animation frame - // ticks). In this case, don't schedule an opportunity, just wait for the next - // one. - if self.documents.borrow().iter().any(|(_, document)| { - document.is_fully_active() && - (document.animations().running_animation_count() != 0 || - document.has_active_request_animation_frame_callbacks()) - }) { + 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; } - let Some((_, document)) = self.documents.borrow().iter().find(|(_, document)| { - document.is_fully_active() && - !document.window().layout_blocked() && - !document.restyle_reason().is_empty() - }) else { - return; - }; - - // Queues a task to update the rendering. - // - // - // 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 { + // If no document needs a rendering update, exit early to avoid doing more work. + if !self + .documents + .borrow() + .iter() + .any(|(_, document)| document.needs_rendering_update()) + { 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.window().throttled() && - (document.animations().running_animation_count() != 0 || - document.has_active_request_animation_frame_callbacks()) - }) { + // Wait 20 milliseconds between frames triggered by the script thread itself. This + // should, in theory, allow compositor-based ticks to arrive sooner. + const SCRIPT_THREAD_ANIMATION_TICK_DELAY: Duration = Duration::from_millis(20); + let time_since_last_rendering_opportunity = self + .last_render_opportunity_time + .get() + .map(|last_render_opportunity_time| Instant::now() - last_render_opportunity_time) + .unwrap_or(Duration::MAX); + + // If it's been more than the time of a single frame the last rendering opportunity, + // just run it now. + if time_since_last_rendering_opportunity > SCRIPT_THREAD_ANIMATION_TICK_DELAY { + self.update_the_rendering(can_gc); return; } - /// The amount of time between ScriptThread animation ticks when nothing is - /// changing. In order to be more efficient, only tick at around 30 frames a - /// second, which also gives time for any renderer ticks to come in and cancel - /// this tick. A renderer tick might happen for a variety of reasons, such as a - /// Pipeline in another ScriptThread producing a display list. - const SCRIPT_THREAD_ANIMATION_TICK_DELAY: u64 = 30; - - debug!("Scheduling ScriptThread animation frame."); - 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: Duration::from_millis(SCRIPT_THREAD_ANIMATION_TICK_DELAY), - }); - - let mut scheduled_script_thread_animation_timer = - self.scheduled_script_thread_animation_timer.borrow_mut(); - assert!( - scheduled_script_thread_animation_timer.is_none(), - "Should never schedule a new timer when one is already scheduled." + 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. @@ -1676,10 +1649,7 @@ impl ScriptThread { docs.clear(); } - // Update the rendering whenever we receive an IPC message. This may not actually do anything if - // we are running animations and the compositor hasn't requested a new frame yet via a TickAllAnimatons - // message. - self.update_the_rendering(can_gc); + self.maybe_schedule_rendering_opportunity_after_ipc_message(can_gc); true } diff --git a/components/script/task_manager.rs b/components/script/task_manager.rs index b81ec1e86f0..2c012416165 100644 --- a/components/script/task_manager.rs +++ b/components/script/task_manager.rs @@ -145,7 +145,6 @@ impl TaskManager { task_source_functions!(self, performance_timeline_task_source, PerformanceTimeline); task_source_functions!(self, port_message_queue, PortMessage); 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, user_interaction_task_source, UserInteraction); task_source_functions!(self, websocket_task_source, WebSocket);