Repository: servo/servo (language: python)

Issue:
Fix HTML event-loop
Our current implementation of the [HTML event-loop](https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-processing-model) is in need of fixing. For this issue I am referring only to the window event-loop, which requires interacting with the rendering. Worker loops seem ok for now(we're not delivering rafs to workers yet).

--------------

Quick recap on how it is specified:

1. The event-loop picks a runnable task, and runs it. 
2. It runs a micro-task checkpoint(so one after each task is run)
3. It  reports  long tasks.
4. It runs the idle callback logic.

In parallel(which means anywhere else in Servo, like the compositor), we must wait for for a [rendering opportunity](https://html.spec.whatwg.org/multipage/#event-loop-processing-model:rendering-opportunity) for that event-loop, and then queue a task that runs the [update the rendering steps](https://html.spec.whatwg.org/multipage/#update-the-rendering).

The task queued is then appended to a task queue, and eventually handled at 1 above. Note that this tasks runs a lot of steps, but atomically within the same task, and in a specific order. 

For example, step 11 is [update animations and send events](https://drafts.csswg.org/web-animations-1/#update-animations-and-send-events), step 14 is [update animations and send events](https://drafts.csswg.org/web-animations-1/#update-animations-and-send-events), and step 15 runs the resize observer logic. 

Running these "update the rendering" steps in that way allows for a certain level of determinism. For example, if one requests an animation frame callback from within the last notification callback of a resize observer, one can be certain that by the time the next resize notification is invoked, the animation frame callback will have been invoked first. This [resize observer test](https://github.com/servo/servo/blob/e15262c5a03f78f480282900d6828be19774ced3/tests/wpt/tests/resize-observer/eventloop.html) is an example of relying on that ordering. 

--------------

Quick recap on how the current implementation:
1. We [select](https://github.com/servo/servo/blob/e15262c5a03f78f480282900d6828be19774ced3/components/script/script_thread.rs#L1490) on various channels, including one representing the various task queues, but also other channels representing messages from the constellation/compositor, the image cache, webgpu, and others. 
2. We handle certain type of messages [first](https://github.com/servo/servo/blob/e15262c5a03f78f480282900d6828be19774ced3/components/script/script_thread.rs#L1519). 
7. We then [check for additional messages](https://github.com/servo/servo/blob/e15262c5a03f78f480282900d6828be19774ced3/components/script/script_thread.rs#L1605)
8. We then [`update_animations_and_send_events`](https://github.com/servo/servo/blob/e15262c5a03f78f480282900d6828be19774ced3/components/script/script_thread.rs#L1627)
9. We then process events collected at 1 and 3 above [sequentially](https://github.com/servo/servo/blob/e15262c5a03f78f480282900d6828be19774ced3/components/script/script_thread.rs#L1631)
10. We then perform a [micro task checkpoint](https://github.com/servo/servo/blob/e15262c5a03f78f480282900d6828be19774ced3/components/script/script_thread.rs#L1681)
11. We then [`maybe_queue_document_completion`](https://github.com/servo/servo/blob/e15262c5a03f78f480282900d6828be19774ced3/components/script/script_thread.rs#L1689)
12. Finally, we [issue any batched reflows](https://github.com/servo/servo/blob/e15262c5a03f78f480282900d6828be19774ced3/components/script/script_thread.rs#L1699)

The [ResizeObserver PR](https://github.com/servo/servo/pull/31108) adds a step before the reflows, that runs the resize observer notification steps. 

--------------

The problem: our current implementation does not follow the ordering of the spec, hence it is a source of timing issues. 

Solution: restructure the event-loop to only run the 4 steps it is supposed to run, and restructure the handling of all other messages as happening via a task running the "update the rendering" steps. 


Discussion / hints:
https://github.com/servo/servo/pull/13864 is one prior attempt to make the script thread's event loop resemble the spec 
I can't help but feel like we're trying to re-implement an async runtime on Servo, but without using futures. Has there been any discussion around using async Rust and its related crates to implement the event loop?
~My gut feeling is that async doesn't help here—the goal is still running all code synchronously, not suspending or preempting operations in order to run something.~

I wasn't thinking about web specifications that involve multiple tasks and waiting on the results of operations. My biggest fear about trying to incorporate async into these is integrating them safely with the JS garbage collector.
My naïve thinking is that we can just make sure any JS pointer used by a task is rooted, although this likely comes at a perf cost. Aside from that, I think it's also true that the GC is free to move objects around, so I'm not sure whether making everything rooted is sufficient. Futures in Rust also requires all the pointers to be `Pin`ned as well, so this is indeed tricky.
The first thing I will do, is re-shuffling code so as to wrap all the "update the rendering" logic inside a single task(as described above) to match the spec. 

Regarding using async, my current point of view is that it's not a good idea, because:

- The script thread already implements a custom "async runtime": one that runs Javascript code asynchronously and sequentially. Async Rust is not about running Javascript, it is about running Rust. We could restructure our "script thread" as an async "script task" using async Rust, but it's not clear to me what problem that would solve. 
- Specifically regarding integration with Spidermonkey and garbage collection: yes I agree it would be very difficult, in part because  most Rust async tasks come with a `Send` bound([example](https://docs.rs/tokio/latest/tokio/task/fn.spawn.html) from tokio tasks), and, with the exception of the [`Trusted`](https://github.com/servo/servo/blob/e76770202c059202889bdb3555e9efb817b6f68e/components/script/dom/bindings/refcounted.rs#L77) stuff, our current rooting stack is not `Send`. 
- The HTML event-loop runs tasks sequentially, and multiple event-loops will in practice run in different processes. Async Rust is mostly about running tasks on a single-process threadpool(although single-threaded variants exists). The multi-process nature of modern web engines would exclude the possibility of running multiple HTML event-loops onto a single Rust async runtime. Also, although I don't have a link handy, Chromium have had issues with spec compliance and their async task-like concept of "runnables". Implementing an HTML event-loop within a Rust async runtime would require some way of ensuring sequential execution of tasks, i.e. wrapping task queues in a mutex, so the end-result would be equivalent to running the HTML event-loop on a single thread(but in my opinion more complicated).
- Since doing such a restructuring would be a huge task(no pun intended), it does not seem worth it. 

As a side note, I think there is one part were async could be more useful: the public API of the servo lib, so as to enable the embedding layer to be async and select on different async event sources(as opposed to [draining vectors](https://github.com/servo/servo/blob/e76770202c059202889bdb3555e9efb817b6f68e/components/servo/lib.rs#L832) of events like we currently do), and also to make it easier for embedders to run Servo within an async context. The problem this would solve is the complexity of the main embedder thread, whose [event-loop logic](https://github.com/servo/servo/blob/e76770202c059202889bdb3555e9efb817b6f68e/ports/servoshell/app.rs#L60) is hard to follow because it consists of multiple "event-handling points", see all the `get_events` and `handle_events` calls found in [`App::handle_events`](https://github.com/servo/servo/blob/e76770202c059202889bdb3555e9efb817b6f68e/ports/servoshell/app.rs#L430).
A single async select loop would be a big gain in terms of simplicity, and also since async is so poplular, many embedders may be running in an async context to begin with. Note that this does not mean that even `components/servo` should be async, but rather that only its public API could consists of futures and streams(could open a dedicated issue to discuss). 

cc @wusyong 
One of the problems that I keep encountering is the ability to express the idea to pause execution of the current task and run others that are in the queue, until the current task can resume based on certain conditions.
This is easily expressed with `await` in async Rust, but AFAIK this still cannot be expressed in the current event loop implementation. Perhaps what we need is some sort of function or syntax to perform suspension however, we don't necessarily need to import async Rust wholesale.
> As a side note, I think there is one part were async could be more useful: the public API of the servo lib, so as to enable the embedding layer to be async and select on different async event sources(as opposed to [draining vectors](https://github.com/servo/servo/blob/e76770202c059202889bdb3555e9efb817b6f68e/components/servo/lib.rs#L832) of events like we currently do), and also to make it easier for embedders to run Servo within an async context. The problem this would solve is the complexity of the main embedder thread, whose [event-loop logic](https://github.com/servo/servo/blob/e76770202c059202889bdb3555e9efb817b6f68e/ports/servoshell/app.rs#L60) is hard to follow because it consists of multiple "event-handling points", see all the `get_events` and `handle_events` calls found in [`App::handle_events`](https://github.com/servo/servo/blob/e76770202c059202889bdb3555e9efb817b6f68e/ports/servoshell/app.rs#L430). A single async select loop would be a big gain in terms of simplicity, and also since async is so poplular, many embedders may be running in an async context to begin with. Note that this does not mean that even `components/servo` should be async, but rather that only its public API could consists of futures and streams(could open a dedicated issue to discuss).

I'm not satisfied with `get_event`/`handle_event` API either, but I think async will make it worse. There will be embedding use cases which prefer single thread event loop and limited resources. I also hope the interface could be a single select loop, but perhaps servo lib could just provide the tools to do it. It should be up to embedders to decide whether to build the async context themselves.
> but I think async will make it worse

@wusyong Thanks for your input! Let's see how we can find ways to actually improve the situation in the future...

> This is easily expressed with await in async Rust

Yes I understand what you mean @KiChjang, and this goes straight to the core of my earlier argument: async Rust can use `await` to simplify what otherwise would require some kind of state machine, but this is because the async Rust is "the code run by the runtime". But in our case our Rust code **is the runtime**, so instead of calling `await`, we have to keep the HTML loop running and track state changes with a state machine. It is the JS we run that can remain unaware of that. 

One can imagine splitting our Rust code into a layer of async Rust code on top of the JS, which could use `await` to simplify some of the current manual state machine-like stuff we do(see for example all use of the IPC router), and which would be driven by a "runtime layer" on top. This would be a huge endeavor to implement obviously...

Fix patch:
diff --git a/components/layout_thread/lib.rs b/components/layout_thread/lib.rs
index 674ef2520b5f5..8f23984ed0b96 100644
--- a/components/layout_thread/lib.rs
+++ b/components/layout_thread/lib.rs
@@ -59,7 +59,7 @@ use profile_traits::time::{
     self as profile_time, TimerMetadata, TimerMetadataFrameType, TimerMetadataReflowType,
 };
 use profile_traits::{path, time_profile};
-use script::layout_dom::{ServoLayoutDocument, ServoLayoutElement, ServoLayoutNode};
+use script::layout_dom::{ServoLayoutElement, ServoLayoutNode};
 use script_layout_interface::wrapper_traits::LayoutNode;
 use script_layout_interface::{
     Layout, LayoutConfig, LayoutFactory, NodesFromPointQueryType, OffsetParentResponse, Reflow,
@@ -844,7 +844,6 @@ impl LayoutThread {
         &self,
         data: &Reflow,
         reflow_goal: &ReflowGoal,
-        document: Option<&ServoLayoutDocument>,
         layout_root: &mut dyn Flow,
         layout_context: &mut LayoutContext,
     ) {
@@ -901,18 +900,8 @@ impl LayoutThread {
                 }
 
                 if !reflow_goal.needs_display() {
-                    // Defer the paint step until the next ForDisplay.
-                    //
-                    // We need to tell the document about this so it doesn't
-                    // incorrectly suppress reflows. See #13131.
-                    document
-                        .expect("No document in a non-display reflow?")
-                        .needs_paint_from_layout();
                     return;
                 }
-                if let Some(document) = document {
-                    document.will_paint();
-                }
 
                 let mut display_list = self.display_list.borrow_mut();
                 let display_list = display_list.as_mut().unwrap();
@@ -1176,7 +1165,6 @@ impl LayoutThread {
                 &mut root_flow,
                 &data.reflow_info,
                 &data.reflow_goal,
-                Some(&document),
                 &mut layout_context,
                 thread_pool,
             );
@@ -1251,7 +1239,6 @@ impl LayoutThread {
         root_flow: &mut FlowRef,
         data: &Reflow,
         reflow_goal: &ReflowGoal,
-        document: Option<&ServoLayoutDocument>,
         context: &mut LayoutContext,
         thread_pool: Option<&rayon::ThreadPool>,
     ) {
@@ -1337,7 +1324,7 @@ impl LayoutThread {
             },
         );
 
-        self.perform_post_main_layout_passes(data, root_flow, reflow_goal, document, context);
+        self.perform_post_main_layout_passes(data, root_flow, reflow_goal, context);
     }
 
     fn perform_post_main_layout_passes(
@@ -1345,14 +1332,12 @@ impl LayoutThread {
         data: &Reflow,
         root_flow: &mut FlowRef,
         reflow_goal: &ReflowGoal,
-        document: Option<&ServoLayoutDocument>,
         layout_context: &mut LayoutContext,
     ) {
         // Build the display list if necessary, and send it to the painter.
         self.compute_abs_pos_and_build_display_list(
             data,
             reflow_goal,
-            document,
             FlowRef::deref_mut(root_flow),
             &mut *layout_context,
         );
diff --git a/components/layout_thread_2020/lib.rs b/components/layout_thread_2020/lib.rs
index 5e0415c05e063..326e8a2eda39c 100644
--- a/components/layout_thread_2020/lib.rs
+++ b/components/layout_thread_2020/lib.rs
@@ -47,7 +47,7 @@ use profile_traits::time::{
     self as profile_time, TimerMetadata, TimerMetadataFrameType, TimerMetadataReflowType,
 };
 use profile_traits::{path, time_profile};
-use script::layout_dom::{ServoLayoutDocument, ServoLayoutElement, ServoLayoutNode};
+use script::layout_dom::{ServoLayoutElement, ServoLayoutNode};
 use script_layout_interface::{
     Layout, LayoutConfig, LayoutFactory, NodesFromPointQueryType, OffsetParentResponse,
     ReflowComplete, ReflowGoal, ScriptReflow, TrustedNodeAddress,
@@ -899,7 +899,6 @@ impl LayoutThread {
             self.perform_post_style_recalc_layout_passes(
                 root.clone(),
                 &data.reflow_goal,
-                Some(&document),
                 &mut layout_context,
             );
         }
@@ -929,7 +928,6 @@ impl LayoutThread {
         &self,
         fragment_tree: Arc<FragmentTree>,
         reflow_goal: &ReflowGoal,
-        document: Option<&ServoLayoutDocument>,
         context: &mut LayoutContext,
     ) {
         Self::cancel_animations_for_nodes_not_in_fragment_tree(
@@ -944,20 +942,9 @@ impl LayoutThread {
         }
 
         if !reflow_goal.needs_display_list() {
-            // Defer the paint step until the next ForDisplay.
-            //
-            // We need to tell the document about this so it doesn't
-            // incorrectly suppress reflows. See #13131.
-            document
-                .expect("No document in a non-display reflow?")
-                .needs_paint_from_layout();
             return;
         }
 
-        if let Some(document) = document {
-            document.will_paint();
-        }
-
         let mut epoch = self.epoch.get();
         epoch.next();
         self.epoch.set(epoch);
diff --git a/components/script/dom/document.rs b/components/script/dom/document.rs
index d216b1364faef..0651f934ef508 100644
--- a/components/script/dom/document.rs
+++ b/components/script/dom/document.rs
@@ -345,8 +345,10 @@ pub struct Document {
     /// Information on elements needing restyle to ship over to layout when the
     /// time comes.
     pending_restyles: DomRefCell<HashMap<Dom<Element>, NoTrace<PendingRestyle>>>,
-    /// This flag will be true if layout suppressed a reflow attempt that was
-    /// needed in order for the page to be painted.
+    /// This flag will be true if the `Document` needs to be painted again
+    /// during the next full layout attempt due to some external change such as
+    /// the web view changing size, or because the previous layout was only for
+    /// layout queries (which do not trigger display).
     needs_paint: Cell<bool>,
     /// <http://w3c.github.io/touch-events/#dfn-active-touch-point>
     active_touch_points: DomRefCell<Vec<Dom<Touch>>>,
@@ -823,8 +825,8 @@ impl Document {
         }
     }
 
-    pub fn needs_paint(&self) -> bool {
-        self.needs_paint.get()
+    pub(crate) fn set_needs_paint(&self, value: bool) {
+        self.needs_paint.set(value)
     }
 
     pub fn needs_reflow(&self) -> Option<ReflowTriggerCondition> {
@@ -844,7 +846,7 @@ impl Document {
             return Some(ReflowTriggerCondition::PendingRestyles);
         }
 
-        if self.needs_paint() {
+        if self.needs_paint.get() {
             return Some(ReflowTriggerCondition::PaintPostponed);
         }
 
@@ -3169,8 +3171,6 @@ pub enum DocumentSource {
 #[allow(unsafe_code)]
 pub trait LayoutDocumentHelpers<'dom> {
     fn is_html_document_for_layout(&self) -> bool;
-    fn needs_paint_from_layout(self);
-    fn will_paint(self);
     fn quirks_mode(self) -> QuirksMode;
     fn style_shared_lock(self) -> &'dom StyleSharedRwLock;
     fn shadow_roots(self) -> Vec<LayoutDom<'dom, ShadowRoot>>;
@@ -3185,16 +3185,6 @@ impl<'dom> LayoutDocumentHelpers<'dom> for LayoutDom<'dom, Document> {
         self.unsafe_get().is_html_document
     }
 
-    #[inline]
-    fn needs_paint_from_layout(self) {
-        (self.unsafe_get()).needs_paint.set(true)
-    }
-
-    #[inline]
-    fn will_paint(self) {
-        (self.unsafe_get()).needs_paint.set(false)
-    }
-
     #[inline]
     fn quirks_mode(self) -> QuirksMode {
         self.unsafe_get().quirks_mode.get()
@@ -4171,14 +4161,9 @@ impl Document {
 
     pub(crate) fn maybe_mark_animating_nodes_as_dirty(&self) {
         let current_timeline_value = self.current_animation_timeline_value();
-        let marked_dirty = self
-            .animations
+        self.animations
             .borrow()
             .mark_animating_nodes_as_dirty(current_timeline_value);
-
-        if marked_dirty {
-            self.window().add_pending_reflow();
-        }
     }
 
     pub(crate) fn current_animation_timeline_value(&self) -> f64 {
diff --git a/components/script/dom/htmliframeelement.rs b/components/script/dom/htmliframeelement.rs
index 904060df341d0..0e57e4ad26c72 100644
--- a/components/script/dom/htmliframeelement.rs
+++ b/components/script/dom/htmliframeelement.rs
@@ -442,8 +442,6 @@ impl HTMLIFrameElement {
         }
 
         self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage);
-        let window = window_from_node(self);
-        window.add_pending_reflow();
     }
 
     fn new_inherited(
diff --git a/components/script/dom/htmlimageelement.rs b/components/script/dom/htmlimageelement.rs
index 112115e0442c9..c23e02d567220 100644
--- a/components/script/dom/htmlimageelement.rs
+++ b/components/script/dom/htmlimageelement.rs
@@ -503,9 +503,7 @@ impl HTMLImageElement {
                 .fire_event(atom!("loadend"), can_gc);
         }
 
-        // Trigger reflow
-        let window = window_from_node(self);
-        window.add_pending_reflow();
+        self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage);
     }
 
     fn process_image_response_for_environment_change(
diff --git a/components/script/dom/window.rs b/components/script/dom/window.rs
index cfe9277504390..5109ffc35107d 100644
--- a/components/script/dom/window.rs
+++ b/components/script/dom/window.rs
@@ -173,14 +173,12 @@ pub enum ReflowReason {
     CachedPageNeededReflow,
     ElementStateChanged,
     FirstLoad,
-    MissingExplicitReflow,
-    PendingReflow,
     Query,
     RefreshTick,
     RequestAnimationFrame,
     ScrollFromScript,
+    UpdateTheRendering,
     Viewport,
-    WindowResize,
     WorkletLoaded,
 }
 
@@ -257,9 +255,6 @@ pub struct Window {
     /// suppress others like MissingExplicitReflow.
     suppress_reflow: Cell<bool>,
 
-    /// A counter of the number of pending reflows for this window.
-    pending_reflow_count: Cell<u32>,
-
     /// A channel for communicating results of async scripts back to the webdriver server
     #[ignore_malloc_size_of = "channels are hard"]
     #[no_trace]
@@ -529,7 +524,6 @@ impl Window {
                 nodes.remove();
             },
         }
-        self.add_pending_reflow();
     }
 
     pub fn compositor_api(&self) -> &CrossProcessCompositorApi {
@@ -1831,9 +1825,12 @@ impl Window {
             _ => (),
         }
 
-        let for_display = reflow_goal == ReflowGoal::Full;
+        let for_display = reflow_goal.needs_display();
         let pipeline_id = self.upcast::<GlobalScope>().pipeline_id();
-        if for_display && self.suppress_reflow.get() {
+
+        // If this was just a normal reflow (not triggered by script which forces reflow),
+        // and the document is still suppressing reflows, exit early.
+        if reflow_goal == ReflowGoal::Full && self.suppress_reflow.get() {
             debug!(
                 "Suppressing reflow pipeline {} for reason {:?} before FirstLoad or RefreshTick",
                 pipeline_id, reason
@@ -1891,7 +1888,6 @@ impl Window {
             .map(|root| root.upcast::<Node>().to_trusted_node_address());
 
         // Send new document and relevant styles to layout.
-        let needs_display = reflow_goal.needs_display();
         let reflow = ScriptReflow {
             reflow_info: Reflow {
                 page_clip_rect: self.page_clip_rect.get(),
@@ -1924,16 +1920,16 @@ impl Window {
 
         debug!("script: layout complete");
 
-        // Pending reflows require display, so only reset the pending reflow count if this reflow
-        // was to be displayed.
-        if needs_display {
-            self.pending_reflow_count.set(0);
-        }
-
         if let Some(marker) = marker {
             self.emit_timeline_marker(marker.end());
         }
 
+        // Either this reflow caused new contents to be displayed or on the next
+        // full layout attempt a reflow should be forced in order to update the
+        // visual contents of the page. A case where full display might be delayed
+        // is when reflowing just for the purpose of doing a layout query.
+        document.set_needs_paint(!for_display);
+
         for image in complete.pending_images {
             let id = image.id;
             let node = unsafe { from_untrusted_node_address(image.node) };
@@ -2365,15 +2361,6 @@ impl Window {
         self.dom_static.windowproxy_handler
     }
 
-    pub fn get_pending_reflow_count(&self) -> u32 {
-        self.pending_reflow_count.get()
-    }
-
-    pub fn add_pending_reflow(&self) {
-        self.pending_reflow_count
-            .set(self.pending_reflow_count.get() + 1);
-    }
-
     pub fn add_resize_event(&self, event: WindowSizeData, event_type: WindowSizeType) {
         // Whenever we receive a new resize event we forget about all the ones that came before
         // it, to avoid unnecessary relayouts
@@ -2407,6 +2394,10 @@ impl Window {
 
         self.page_clip_rect.set(proposed_clip_rect);
 
+        // The document needs to be repainted, because the initial containing block
+        // is now a different size.
+        self.Document().set_needs_paint(true);
+
         // If we didn't have a clip rect, the previous display doesn't need rebuilding
         // because it was built for infinite clip (MaxRect::amax_rect()).
         had_clip_rect
@@ -2504,9 +2495,6 @@ impl Window {
         );
         self.set_window_size(new_size);
 
-        // TODO: This should just trigger a pending reflow instead of forcing one now.
-        self.force_reflow(ReflowGoal::Full, ReflowReason::WindowResize, None);
-
         // http://dev.w3.org/csswg/cssom-view/#resizing-viewports
         if size_type == WindowSizeType::Resize {
             let uievent = UIEvent::new(
@@ -2521,6 +2509,10 @@ impl Window {
             uievent.upcast::<Event>().fire(self.upcast(), can_gc);
         }
 
+        // The document needs to be repainted, because the initial containing block
+        // is now a different size.
+        self.Document().set_needs_paint(true);
+
         true
     }
 
@@ -2707,7 +2699,6 @@ impl Window {
             window_size: Cell::new(window_size),
             current_viewport: Cell::new(initial_viewport.to_untyped()),
             suppress_reflow: Cell::new(true),
-            pending_reflow_count: Default::default(),
             current_state: Cell::new(WindowState::Alive),
             devtools_marker_sender: Default::default(),
             devtools_markers: Default::default(),
diff --git a/components/script/layout_dom/document.rs b/components/script/layout_dom/document.rs
index c3cbd7a6c22f6..c2e556ad7fc1e 100644
--- a/components/script/layout_dom/document.rs
+++ b/components/script/layout_dom/document.rs
@@ -49,14 +49,6 @@ impl<'ld> ServoLayoutDocument<'ld> {
             .next()
     }
 
-    pub fn needs_paint_from_layout(&self) {
-        self.document.needs_paint_from_layout()
-    }
-
-    pub fn will_paint(&self) {
-        self.document.will_paint()
-    }
-
     pub fn style_shared_lock(&self) -> &StyleSharedRwLock {
         self.document.style_shared_lock()
     }
diff --git a/components/script/script_thread.rs b/components/script/script_thread.rs
index f5d5b8fb09bbd..50ed33fee9f84 100644
--- a/components/script/script_thread.rs
+++ b/components/script/script_thread.rs
@@ -1635,9 +1635,8 @@ impl ScriptThread {
             // > Step 22: For each doc of docs, update the rendering or user interface of
             // > doc and its node navigable to reflect the current state.
             let window = document.window();
-            let pending_reflows = window.get_pending_reflow_count();
-            if document.is_fully_active() && pending_reflows > 0 {
-                window.reflow(ReflowGoal::Full, ReflowReason::PendingReflow, can_gc);
+            if document.is_fully_active() {
+                window.reflow(ReflowGoal::Full, ReflowReason::UpdateTheRendering, can_gc);
             }
 
             // TODO: Process top layer removals according to
@@ -1790,7 +1789,7 @@ impl ScriptThread {
                 },
                 FromConstellation(ConstellationControlMsg::Viewport(id, rect)) => self
                     .profile_event(ScriptThreadEventCategory::SetViewport, Some(id), || {
-                        self.handle_viewport(id, rect, can_gc);
+                        self.handle_viewport(id, rect);
                     }),
                 FromConstellation(ConstellationControlMsg::TickAllAnimations(
                     pipeline_id,
@@ -2492,7 +2491,7 @@ impl ScriptThread {
                 self.collect_reports(chan)
             },
             MainThreadScriptMsg::WorkletLoaded(pipeline_id) => {
-                self.handle_worklet_loaded(pipeline_id, CanGc::note())
+                self.handle_worklet_loaded(pipeline_id)
             },
             MainThreadScriptMsg::RegisterPaintWorklet {
                 pipeline_id,
@@ -2867,12 +2866,10 @@ impl ScriptThread {
         }
     }
 
-    fn handle_viewport(&self, id: PipelineId, rect: Rect<f32>, can_gc: CanGc) {
+    fn handle_viewport(&self, id: PipelineId, rect: Rect<f32>) {
         let document = self.documents.borrow().find_document(id);
         if let Some(document) = document {
-            if document.window().set_page_clip_rect_with_new_viewport(rect) {
-                self.rebuild_and_force_reflow(&document, ReflowReason::Viewport, can_gc);
-            }
+            document.window().set_page_clip_rect_with_new_viewport(rect);
             return;
         }
         let loads = self.incomplete_loads.borrow();
@@ -3381,7 +3378,6 @@ impl ScriptThread {
 
         // TODO: This should only dirty nodes that are waiting for a web font to finish loading!
         document.dirty_all_nodes();
-        document.window().add_pending_reflow();
 
         // This is required because the handlers added to the promise exposed at
         // `document.fonts.ready` are run by the event loop only when it performs a microtask
@@ -3390,11 +3386,11 @@ impl ScriptThread {
         self.rendering_opportunity(pipeline_id);
     }
 
-    /// Handles a worklet being loaded. Does nothing if the page no longer exists.
-    fn handle_worklet_loaded(&self, pipeline_id: PipelineId, can_gc: CanGc) {
-        let document = self.documents.borrow().find_document(pipeline_id);
-        if let Some(document) = document {
-            self.rebuild_and_force_reflow(&document, ReflowReason::WorkletLoaded, can_gc);
+    /// Handles a worklet being loaded by triggering a relayout of the page. Does nothing if the
+    /// page no longer exists.
+    fn handle_worklet_loaded(&self, pipeline_id: PipelineId) {
+        if let Some(document) = self.documents.borrow().find_document(pipeline_id) {
+            document.set_needs_paint(true)
         }
     }
 
@@ -3857,13 +3853,6 @@ impl ScriptThread {
         }
     }
 
-    /// Reflows non-incrementally, rebuilding the entire layout tree in the process.
-    fn rebuild_and_force_reflow(&self, document: &Document, reason: ReflowReason, can_gc: CanGc) {
-        let window = window_from_node(document);
-        document.dirty_all_nodes();
-        window.reflow(ReflowGoal::Full, reason, can_gc);
-    }
-
     /// Queue compositor events for later dispatching as part of a
     /// `update_the_rendering` task.
     fn handle_event(&self, pipeline_id: PipelineId, event: CompositorEvent) {


Test patch:
diff --git a/tests/wpt/meta/css/css-ui/transparent-accent-color-001.html.ini b/tests/wpt/meta/css/css-ui/transparent-accent-color-001.html.ini
deleted file mode 100644
index 86d0d1e6e2c9c..0000000000000
--- a/tests/wpt/meta/css/css-ui/transparent-accent-color-001.html.ini
+++ /dev/null
@@ -1,2 +0,0 @@
-[transparent-accent-color-001.html]
-  expected: TIMEOUT
diff --git a/tests/wpt/meta/css/css-ui/transparent-accent-color-002.html.ini b/tests/wpt/meta/css/css-ui/transparent-accent-color-002.html.ini
deleted file mode 100644
index 5942e38a2dbaa..0000000000000
--- a/tests/wpt/meta/css/css-ui/transparent-accent-color-002.html.ini
+++ /dev/null
@@ -1,2 +0,0 @@
-[transparent-accent-color-002.html]
-  expected: TIMEOUT