layout: Introduce ReflowPhasesRun (#38467)

There were various booleans on `ReflowResults` that represented various
actions that might have been taken during a reflow request. Replace
those with a bitflags that better represents what reflow phases have
actually been run. Update variable names to reflect what they mean.

In addition, run some post-layout tasks unconditionally. They are
already contingent on the results returned from layout.

This simplifies and clarifies the code a good deal.

Testing: This should not change observable behavior and thus is covered
by existing WPT tests.

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
This commit is contained in:
Martin Robinson 2025-08-05 10:39:42 +02:00 committed by GitHub
parent 92a9d24a13
commit 3e856cbf11
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 100 additions and 113 deletions

View file

@ -27,8 +27,8 @@ use fxhash::FxHashMap;
use ipc_channel::ipc::IpcSender;
use layout_api::{
IFrameSizes, Layout, LayoutConfig, LayoutDamage, LayoutFactory, NodesFromPointQueryType,
OffsetParentResponse, QueryMsg, ReflowGoal, ReflowRequest, ReflowRequestRestyle, ReflowResult,
TrustedNodeAddress,
OffsetParentResponse, QueryMsg, ReflowGoal, ReflowPhasesRun, ReflowRequest,
ReflowRequestRestyle, ReflowResult, TrustedNodeAddress,
};
use log::{debug, error, warn};
use malloc_size_of::{MallocConditionalSizeOf, MallocSizeOf, MallocSizeOfOps};
@ -672,13 +672,13 @@ impl LayoutThread {
self.maybe_print_reflow_event(&reflow_request);
if self.can_skip_reflow_request_entirely(&reflow_request) {
// We could skip the layout, but we might need to update the scroll node.
let update_scroll_reflow_target_scrolled =
self.handle_update_scroll_node_request(&reflow_request);
return Some(ReflowResult::new_without_relayout(
update_scroll_reflow_target_scrolled,
));
// We can skip layout, but we might need to update a scroll node.
return self
.handle_update_scroll_node_request(&reflow_request)
.then(|| ReflowResult {
reflow_phases_run: ReflowPhasesRun::UpdatedScrollNodeOffset,
..Default::default()
});
}
let document = unsafe { ServoLayoutNode::new(&reflow_request.document) };
@ -698,30 +698,34 @@ impl LayoutThread {
animation_timeline_value: reflow_request.animation_timeline_value,
});
let (damage, iframe_sizes) = self.restyle_and_build_trees(
let (mut reflow_phases_run, damage, iframe_sizes) = self.restyle_and_build_trees(
&mut reflow_request,
document,
root_element,
&image_resolver,
);
self.calculate_overflow(damage);
self.build_stacking_context_tree(&reflow_request, damage);
let built_display_list = self.build_display_list(&reflow_request, damage, &image_resolver);
let update_scroll_reflow_target_scrolled =
self.handle_update_scroll_node_request(&reflow_request);
if self.calculate_overflow(damage) {
reflow_phases_run.insert(ReflowPhasesRun::CalculatedOverflow);
}
if self.build_stacking_context_tree(&reflow_request, damage) {
reflow_phases_run.insert(ReflowPhasesRun::BuiltStackingContextTree);
}
if self.build_display_list(&reflow_request, damage, &image_resolver) {
reflow_phases_run.insert(ReflowPhasesRun::BuiltDisplayList);
}
if self.handle_update_scroll_node_request(&reflow_request) {
reflow_phases_run.insert(ReflowPhasesRun::UpdatedScrollNodeOffset);
}
let pending_images = std::mem::take(&mut *image_resolver.pending_images.lock());
let pending_rasterization_images =
std::mem::take(&mut *image_resolver.pending_rasterization_images.lock());
Some(ReflowResult {
built_display_list,
reflow_phases_run,
pending_images,
pending_rasterization_images,
iframe_sizes: Some(iframe_sizes),
update_scroll_reflow_target_scrolled,
processed_relayout: true,
})
}
@ -793,11 +797,11 @@ impl LayoutThread {
document: ServoLayoutDocument<'_>,
root_element: ServoLayoutElement<'_>,
image_resolver: &Arc<ImageResolver>,
) -> (RestyleDamage, IFrameSizes) {
) -> (ReflowPhasesRun, RestyleDamage, IFrameSizes) {
let mut snapshot_map = SnapshotMap::new();
let _snapshot_setter = match reflow_request.restyle.as_mut() {
Some(restyle) => SnapshotSetter::new(restyle, &mut snapshot_map),
None => return (RestyleDamage::empty(), IFrameSizes::default()),
None => return Default::default(),
};
let document_shared_lock = document.style_shared_lock();
@ -877,7 +881,7 @@ impl LayoutThread {
if !token.should_traverse() {
layout_context.style_context.stylist.rule_tree().maybe_gc();
return (RestyleDamage::empty(), IFrameSizes::default());
return Default::default();
}
dirty_root = driver::traverse_dom(&recalc_style_traversal, token, rayon_pool).as_node();
@ -895,7 +899,7 @@ impl LayoutThread {
if !damage.contains(RestyleDamage::RELAYOUT) {
layout_context.style_context.stylist.rule_tree().maybe_gc();
return (damage, IFrameSizes::default());
return (ReflowPhasesRun::empty(), damage, IFrameSizes::default());
}
let mut box_tree = self.box_tree.borrow_mut();
@ -956,13 +960,17 @@ impl LayoutThread {
layout_context.style_context.stylist.rule_tree().maybe_gc();
let mut iframe_sizes = layout_context.iframe_sizes.lock();
(damage, std::mem::take(&mut *iframe_sizes))
(
ReflowPhasesRun::RanLayout,
damage,
std::mem::take(&mut *iframe_sizes),
)
}
#[servo_tracing::instrument(name = "Overflow Calculation", skip_all)]
fn calculate_overflow(&self, damage: RestyleDamage) {
fn calculate_overflow(&self, damage: RestyleDamage) -> bool {
if !damage.contains(RestyleDamage::RECALCULATE_OVERFLOW) {
return;
return false;
}
if let Some(fragment_tree) = &*self.fragment_tree.borrow() {
@ -976,22 +984,27 @@ impl LayoutThread {
// display list the next time one is requested.
self.need_new_display_list.set(true);
self.need_new_stacking_context_tree.set(true);
true
}
#[servo_tracing::instrument(name = "Stacking Context Tree Construction", skip_all)]
fn build_stacking_context_tree(&self, reflow_request: &ReflowRequest, damage: RestyleDamage) {
fn build_stacking_context_tree(
&self,
reflow_request: &ReflowRequest,
damage: RestyleDamage,
) -> bool {
if !ReflowPhases::necessary(&reflow_request.reflow_goal)
.contains(ReflowPhases::StackingContextTreeConstruction)
{
return;
return false;
}
let Some(fragment_tree) = &*self.fragment_tree.borrow() else {
return;
return false;
};
if !damage.contains(RestyleDamage::REBUILD_STACKING_CONTEXT) &&
!self.need_new_stacking_context_tree.get()
{
return;
return false;
}
let mut stacking_context_tree = self.stacking_context_tree.borrow_mut();
@ -1041,6 +1054,8 @@ impl LayoutThread {
);
}
}
true
}
/// Build the display list for the current layout and send it to the renderer. If no display

View file

@ -43,7 +43,8 @@ use ipc_channel::ipc;
use js::rust::{HandleObject, HandleValue, MutableHandleValue};
use keyboard_types::{Code, Key, KeyState, Modifiers, NamedKey};
use layout_api::{
PendingRestyle, ReflowGoal, RestyleReason, TrustedNodeAddress, node_id_from_scroll_id,
PendingRestyle, ReflowGoal, ReflowPhasesRun, RestyleReason, TrustedNodeAddress,
node_id_from_scroll_id,
};
use metrics::{InteractiveFlag, InteractiveWindow, ProgressiveWebMetrics};
use net_traits::CookieSource::NonHTTP;
@ -3694,8 +3695,8 @@ impl Document {
// > Step 22: For each doc of docs, update the rendering or user interface of
// > doc and its node navigable to reflect the current state.
//
// Returns true if a reflow occured.
pub(crate) fn update_the_rendering(&self) -> bool {
// Returns the set of reflow phases run as a [`ReflowPhasesRun`].
pub(crate) fn update_the_rendering(&self) -> ReflowPhasesRun {
self.update_animating_images();
// All dirty canvases are flushed before updating the rendering.
@ -3730,9 +3731,7 @@ impl Document {
receiver.recv().unwrap();
}
self.window()
.reflow(ReflowGoal::UpdateTheRendering)
.reflow_issued
self.window().reflow(ReflowGoal::UpdateTheRendering)
}
/// From <https://drafts.csswg.org/css-font-loading/#fontfaceset-pending-on-the-environment>:

View file

@ -52,8 +52,8 @@ use js::rust::{
};
use layout_api::{
FragmentType, Layout, PendingImage, PendingImageState, PendingRasterizationImage, QueryMsg,
ReflowGoal, ReflowRequest, ReflowRequestRestyle, RestyleReason, TrustedNodeAddress,
combine_id_with_fragment_type,
ReflowGoal, ReflowPhasesRun, ReflowRequest, ReflowRequestRestyle, RestyleReason,
TrustedNodeAddress, combine_id_with_fragment_type,
};
use malloc_size_of::MallocSizeOf;
use media::WindowGLContext;
@ -225,25 +225,6 @@ impl LayoutBlocker {
type PendingImageRasterizationKey = (PendingImageId, DeviceIntSize);
/// Feedbacks of the reflow that is required by the one who is initiating the reflow.
pub(crate) struct WindowReflowResult {
/// Whether the reflow actually happened and it sends a new display list to the embedder.
pub reflow_issued: bool,
/// Whether the reflow is for [ReflowGoal::UpdateScrollNode] and the target is scrolled.
/// Specifically, a node is scrolled whenever the scroll position of it changes. Note
/// that reflow that is cancalled would not scroll the target.
pub update_scroll_reflow_target_scrolled: bool,
}
impl WindowReflowResult {
fn new_empty() -> Self {
WindowReflowResult {
reflow_issued: false,
update_scroll_reflow_target_scrolled: false,
}
}
}
#[dom_struct]
pub(crate) struct Window {
globalscope: GlobalScope,
@ -2165,16 +2146,14 @@ impl Window {
// TODO Step 1
// TODO(mrobinson, #18709): Add smooth scrolling support to WebRender so that we can
// properly process ScrollBehavior here.
let WindowReflowResult {
update_scroll_reflow_target_scrolled,
..
} = self.reflow(ReflowGoal::UpdateScrollNode(scroll_id, Vector2D::new(x, y)));
let reflow_phases_run =
self.reflow(ReflowGoal::UpdateScrollNode(scroll_id, Vector2D::new(x, y)));
// > If the scroll position did not change as a result of the user interaction or programmatic
// > invocation, where no translations were applied as a result, then no scrollend event fires
// > because no scrolling occurred.
// Even though the note mention the scrollend, it is relevant to the scroll as well.
if update_scroll_reflow_target_scrolled {
if reflow_phases_run.contains(ReflowPhasesRun::UpdatedScrollNodeOffset) {
match element {
Some(el) => self.Document().handle_element_scroll_event(el),
None => self.Document().handle_viewport_scroll_event(),
@ -2210,25 +2189,25 @@ impl Window {
///
/// NOTE: This method should almost never be called directly! Layout and rendering updates should
/// happen as part of the HTML event loop via *update the rendering*.
pub(crate) fn reflow(&self, reflow_goal: ReflowGoal) -> WindowReflowResult {
pub(crate) fn reflow(&self, reflow_goal: ReflowGoal) -> ReflowPhasesRun {
let document = self.Document();
// Never reflow inactive Documents.
if !document.is_fully_active() {
return WindowReflowResult::new_empty();
return ReflowPhasesRun::empty();
}
self.Document().ensure_safe_to_run_script_or_layout();
// If layouts are blocked, we block all layouts that are for display only. Other
// layouts (for queries and scrolling) are not blocked, as they do not display
// anything and script excpects the layout to be up-to-date after they run.
// anything and script expects the layout to be up-to-date after they run.
let pipeline_id = self.pipeline_id();
if reflow_goal == ReflowGoal::UpdateTheRendering &&
self.layout_blocker.get().layout_blocked()
{
debug!("Suppressing pre-load-event reflow pipeline {pipeline_id}");
return WindowReflowResult::new_empty();
return ReflowPhasesRun::empty();
}
debug!("script: performing reflow for goal {reflow_goal:?}");
@ -2279,37 +2258,30 @@ impl Window {
highlighted_dom_node: document.highlighted_dom_node().map(|node| node.to_opaque()),
};
let Some(results) = self.layout.borrow_mut().reflow(reflow) else {
return WindowReflowResult::new_empty();
let Some(reflow_result) = self.layout.borrow_mut().reflow(reflow) else {
return ReflowPhasesRun::empty();
};
// We are maintaining the previous behavior of layout where we are skipping these behavior if we are not
// doing layout calculation.
if results.processed_relayout {
debug!("script: layout complete");
if let Some(marker) = marker {
self.emit_timeline_marker(marker.end());
}
self.handle_pending_images_post_reflow(
results.pending_images,
results.pending_rasterization_images,
);
if let Some(iframe_sizes) = results.iframe_sizes {
document
.iframes_mut()
.handle_new_iframe_sizes_after_layout(self, iframe_sizes);
}
document.update_animations_post_reflow();
self.update_constellation_epoch();
} else {
debug!("script: layout-side reflow finished without relayout");
debug!("script: layout complete");
if let Some(marker) = marker {
self.emit_timeline_marker(marker.end());
}
WindowReflowResult {
reflow_issued: results.built_display_list,
update_scroll_reflow_target_scrolled: results.update_scroll_reflow_target_scrolled,
self.handle_pending_images_post_reflow(
reflow_result.pending_images,
reflow_result.pending_rasterization_images,
);
if let Some(iframe_sizes) = reflow_result.iframe_sizes {
document
.iframes_mut()
.handle_new_iframe_sizes_after_layout(self, iframe_sizes);
}
document.update_animations_post_reflow();
self.update_constellation_epoch();
reflow_result.reflow_phases_run
}
pub(crate) fn maybe_send_idle_document_state_to_constellation(&self) {

View file

@ -68,7 +68,9 @@ use js::jsapi::{
};
use js::jsval::UndefinedValue;
use js::rust::ParentRuntime;
use layout_api::{LayoutConfig, LayoutFactory, RestyleReason, ScriptThreadFactory};
use layout_api::{
LayoutConfig, LayoutFactory, ReflowPhasesRun, RestyleReason, ScriptThreadFactory,
};
use media::WindowGLContext;
use metrics::MAX_TASK_NS;
use net_traits::image_cache::{ImageCache, ImageCacheResponseMessage};
@ -1238,6 +1240,8 @@ impl ScriptThread {
///
/// Attempt to update the rendering and then do a microtask checkpoint if rendering was actually
/// updated.
///
/// Returns true if any reflows produced a new display list.
pub(crate) fn update_the_rendering(&self, can_gc: CanGc) -> bool {
self.last_render_opportunity_time.set(Some(Instant::now()));
self.cancel_scheduled_update_the_rendering();
@ -1275,7 +1279,7 @@ impl ScriptThread {
// steps per doc in docs. Currently `<iframe>` resizing depends on a parent being able to
// queue resize events on a child and have those run in the same call to this method, so
// that needs to be sorted out to fix this.
let mut saw_any_reflows = false;
let mut built_any_display_lists = false;
for pipeline_id in documents_in_order.iter() {
let document = self
.documents
@ -1358,7 +1362,10 @@ 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.
saw_any_reflows = document.update_the_rendering() || saw_any_reflows;
built_any_display_lists = document
.update_the_rendering()
.contains(ReflowPhasesRun::BuiltDisplayList) ||
built_any_display_lists;
// TODO: Process top layer removals according to
// https://drafts.csswg.org/css-position-4/#process-top-layer-removals.
@ -1367,7 +1374,7 @@ impl ScriptThread {
// Perform a microtask checkpoint as the specifications says that *update the rendering*
// should be run in a task and a microtask checkpoint is always done when running tasks.
self.perform_a_microtask_checkpoint(can_gc);
saw_any_reflows
built_any_display_lists
}
/// Schedule a rendering update ("update the rendering"), if necessary. This

View file

@ -387,8 +387,8 @@ impl RestyleReason {
/// Information derived from a layout pass that needs to be returned to the script thread.
#[derive(Debug, Default)]
pub struct ReflowResult {
/// Whether or not this reflow produced a display list.
pub built_display_list: bool,
/// The phases that were run during this reflow.
pub reflow_phases_run: ReflowPhasesRun,
/// The list of images that were encountered that are in progress.
pub pending_images: Vec<PendingImage>,
/// The list of vector images that were encountered that still need to be rasterized.
@ -399,23 +399,17 @@ pub struct ReflowResult {
/// finished before reaching this stage of the layout. I.e., no update
/// required.
pub iframe_sizes: Option<IFrameSizes>,
/// Whether the reflow is for [ReflowGoal::UpdateScrollNode] and the target is scrolled.
/// Specifically, a node is scrolled whenever the scroll position of it changes.
pub update_scroll_reflow_target_scrolled: bool,
/// Do the reflow results in a new component within layout. Incremental layout could be
/// skipped if it is deemed unnecessary or the required component is not ready to be
/// processed.
pub processed_relayout: bool,
}
impl ReflowResult {
/// In incremental reflow, we could skip the layout calculation completely, if it is deemed
/// unecessary. In those cases, many of the [ReflowResult] would be irrelevant.
pub fn new_without_relayout(update_scroll_reflow_target_scrolled: bool) -> Self {
ReflowResult {
update_scroll_reflow_target_scrolled,
..Default::default()
}
bitflags! {
/// The phases of reflow that were run when processing a reflow in layout.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct ReflowPhasesRun: u8 {
const RanLayout = 1 << 0;
const CalculatedOverflow = 1 << 1;
const BuiltStackingContextTree = 1 << 2;
const BuiltDisplayList = 1 << 3;
const UpdatedScrollNodeOffset = 1 << 4;
}
}