mirror of
https://github.com/servo/servo.git
synced 2025-08-17 03:15:34 +01:00
Auto merge of #12563 - emilio:stylo, r=bholley,jdm,pcwalton
stylo: Improve restyling performance This commit adds hooks to the Servo style traversal to avoid traversing all the DOM for every restyle. Additionally it changes the behavior of the dirty flag to be propagated top down, to prevent extra overhead when an element is dirtied. This commit doesn't aim to change the behavior on Servo just yet, since Servo does extra job when dirtying the node related with DOM revision counters that might be necessary. CC @asajeffrey for the DOM revision counters stuff. When a node is dirty, do all its descendants really need to increment the revision counter, or is this an unintended effect? My intuition is that this is hurting performance quite a lot for servo. r? @bholley <!-- Please describe your changes on the following line: --> --- <!-- Thank you for contributing to Servo! Please replace each `[ ]` by `[X]` when the step is complete, and replace `__` with appropriate data: --> - [x] `./mach build -d` does not report any errors - [x] `./mach test-tidy` does not report any errors <!-- Either: --> - [x] These changes do not require tests because no geckolib tests yet. <!-- Pull requests that do not address these steps are welcome, but they will require additional verification as part of the review process. --> <!-- Reviewable:start --> --- This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/servo/servo/12563) <!-- Reviewable:end -->
This commit is contained in:
commit
944d371b8f
18 changed files with 195 additions and 177 deletions
|
@ -37,13 +37,13 @@ impl PrivateStyleData {
|
|||
#[cfg_attr(feature = "servo", derive(HeapSizeOf))]
|
||||
pub struct DomParallelInfo {
|
||||
/// The number of children that still need work done.
|
||||
pub children_count: AtomicIsize,
|
||||
pub children_to_process: AtomicIsize,
|
||||
}
|
||||
|
||||
impl DomParallelInfo {
|
||||
pub fn new() -> DomParallelInfo {
|
||||
DomParallelInfo {
|
||||
children_count: AtomicIsize::new(0),
|
||||
children_to_process: AtomicIsize::new(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -111,33 +111,10 @@ pub trait TNode : Sized + Copy + Clone {
|
|||
|
||||
unsafe fn set_dirty_descendants(&self, value: bool);
|
||||
|
||||
fn dirty_self(&self) {
|
||||
unsafe {
|
||||
self.set_dirty(true);
|
||||
self.set_dirty_descendants(true);
|
||||
}
|
||||
}
|
||||
|
||||
fn dirty_descendants(&self) {
|
||||
for ref child in self.children() {
|
||||
child.dirty_self();
|
||||
child.dirty_descendants();
|
||||
}
|
||||
}
|
||||
|
||||
fn needs_dirty_on_viewport_size_changed(&self) -> bool;
|
||||
|
||||
unsafe fn set_dirty_on_viewport_size_changed(&self);
|
||||
|
||||
fn set_descendants_dirty_on_viewport_size_changed(&self) {
|
||||
for ref child in self.children() {
|
||||
unsafe {
|
||||
child.set_dirty_on_viewport_size_changed();
|
||||
}
|
||||
child.set_descendants_dirty_on_viewport_size_changed();
|
||||
}
|
||||
}
|
||||
|
||||
fn can_be_fragmented(&self) -> bool;
|
||||
|
||||
unsafe fn set_can_be_fragmented(&self, value: bool);
|
||||
|
@ -215,7 +192,7 @@ pub trait TElement : Sized + Copy + Clone + ElementExt + PresentationalHintsSynt
|
|||
fn attr_equals(&self, namespace: &Namespace, attr: &Atom, value: &Atom) -> bool;
|
||||
|
||||
/// Properly marks nodes as dirty in response to restyle hints.
|
||||
fn note_restyle_hint(&self, mut hint: RestyleHint) {
|
||||
fn note_restyle_hint(&self, hint: RestyleHint) {
|
||||
// Bail early if there's no restyling to do.
|
||||
if hint.is_empty() {
|
||||
return;
|
||||
|
@ -233,23 +210,21 @@ pub trait TElement : Sized + Copy + Clone + ElementExt + PresentationalHintsSynt
|
|||
|
||||
// Process hints.
|
||||
if hint.contains(RESTYLE_SELF) {
|
||||
node.dirty_self();
|
||||
unsafe { node.set_dirty(true); }
|
||||
// XXX(emilio): For now, dirty implies dirty descendants if found.
|
||||
} else if hint.contains(RESTYLE_DESCENDANTS) {
|
||||
let mut current = node.first_child();
|
||||
while let Some(node) = current {
|
||||
unsafe { node.set_dirty(true); }
|
||||
current = node.next_sibling();
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME(bholley, #8438): We currently need to RESTYLE_DESCENDANTS in the
|
||||
// RESTYLE_SELF case in order to make sure "inherit" style structs propagate
|
||||
// properly. See the explanation in the github issue.
|
||||
hint.insert(RESTYLE_DESCENDANTS);
|
||||
}
|
||||
if hint.contains(RESTYLE_DESCENDANTS) {
|
||||
unsafe { node.set_dirty_descendants(true); }
|
||||
node.dirty_descendants();
|
||||
}
|
||||
if hint.contains(RESTYLE_LATER_SIBLINGS) {
|
||||
let mut next = ::selectors::Element::next_sibling_element(self);
|
||||
while let Some(sib) = next {
|
||||
let sib_node = sib.as_node();
|
||||
sib_node.dirty_self();
|
||||
sib_node.dirty_descendants();
|
||||
unsafe { sib_node.set_dirty(true) };
|
||||
next = ::selectors::Element::next_sibling_element(&sib);
|
||||
}
|
||||
}
|
||||
|
@ -262,12 +237,16 @@ pub struct TreeIterator<ConcreteNode> where ConcreteNode: TNode {
|
|||
|
||||
impl<ConcreteNode> TreeIterator<ConcreteNode> where ConcreteNode: TNode {
|
||||
fn new(root: ConcreteNode) -> TreeIterator<ConcreteNode> {
|
||||
let mut stack = vec!();
|
||||
let mut stack = vec![];
|
||||
stack.push(root);
|
||||
TreeIterator {
|
||||
stack: stack,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next_skipping_children(&mut self) -> Option<ConcreteNode> {
|
||||
self.stack.pop()
|
||||
}
|
||||
}
|
||||
|
||||
impl<ConcreteNode> Iterator for TreeIterator<ConcreteNode>
|
||||
|
|
|
@ -63,25 +63,40 @@ fn top_down_dom<N, C>(unsafe_nodes: UnsafeNodeList,
|
|||
// Get a real layout node.
|
||||
let node = unsafe { N::from_unsafe(&unsafe_node) };
|
||||
|
||||
if !context.should_process(node) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Perform the appropriate traversal.
|
||||
context.process_preorder(node);
|
||||
|
||||
let child_count = node.children_count();
|
||||
// Possibly enqueue the children.
|
||||
let mut children_to_process = 0isize;
|
||||
for kid in node.children() {
|
||||
// Trigger the hook pre-adding the kid to the list. This can (and in
|
||||
// fact uses to) change the result of the should_process operation.
|
||||
//
|
||||
// As of right now, this hook takes care of propagating the restyle
|
||||
// flag down the tree. In the future, more accurate behavior is
|
||||
// probably going to be needed.
|
||||
context.pre_process_child_hook(node, kid);
|
||||
if context.should_process(kid) {
|
||||
children_to_process += 1;
|
||||
discovered_child_nodes.push(kid.to_unsafe())
|
||||
}
|
||||
}
|
||||
|
||||
// Reset the count of children.
|
||||
{
|
||||
let data = node.mutate_data().unwrap();
|
||||
data.parallel.children_count.store(child_count as isize,
|
||||
Ordering::Relaxed);
|
||||
data.parallel.children_to_process
|
||||
.store(children_to_process,
|
||||
Ordering::Relaxed);
|
||||
}
|
||||
|
||||
// Possibly enqueue the children.
|
||||
if child_count != 0 {
|
||||
for kid in node.children() {
|
||||
discovered_child_nodes.push(kid.to_unsafe())
|
||||
}
|
||||
} else {
|
||||
// If there were no more children, start walking back up.
|
||||
|
||||
// If there were no more children, start walking back up.
|
||||
if children_to_process == 0 {
|
||||
bottom_up_dom::<N, C>(unsafe_nodes.1, unsafe_node, proxy)
|
||||
}
|
||||
}
|
||||
|
@ -128,7 +143,7 @@ fn bottom_up_dom<N, C>(root: OpaqueNode,
|
|||
|
||||
if parent_data
|
||||
.parallel
|
||||
.children_count
|
||||
.children_to_process
|
||||
.fetch_sub(1, Ordering::Relaxed) != 1 {
|
||||
// Get out of here and find another node to work on.
|
||||
break
|
||||
|
@ -138,4 +153,3 @@ fn bottom_up_dom<N, C>(root: OpaqueNode,
|
|||
node = parent;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,20 +9,29 @@ use traversal::DomTraversalContext;
|
|||
|
||||
pub fn traverse_dom<N, C>(root: N,
|
||||
shared: &C::SharedContext)
|
||||
where N: TNode,
|
||||
C: DomTraversalContext<N> {
|
||||
where N: TNode,
|
||||
C: DomTraversalContext<N>
|
||||
{
|
||||
fn doit<'a, N, C>(context: &'a C, node: N)
|
||||
where N: TNode, C: DomTraversalContext<N> {
|
||||
where N: TNode,
|
||||
C: DomTraversalContext<N>
|
||||
{
|
||||
debug_assert!(context.should_process(node));
|
||||
context.process_preorder(node);
|
||||
|
||||
for kid in node.children() {
|
||||
doit::<N, C>(context, kid);
|
||||
context.pre_process_child_hook(node, kid);
|
||||
if context.should_process(node) {
|
||||
doit::<N, C>(context, kid);
|
||||
}
|
||||
}
|
||||
|
||||
context.process_postorder(node);
|
||||
}
|
||||
|
||||
let context = C::new(shared, root.opaque());
|
||||
doit::<N, C>(&context, root);
|
||||
if context.should_process(root) {
|
||||
doit::<N, C>(&context, root);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -147,6 +147,30 @@ pub trait DomTraversalContext<N: TNode> {
|
|||
fn process_preorder(&self, node: N);
|
||||
/// Process `node` on the way up, after its children have been processed.
|
||||
fn process_postorder(&self, node: N);
|
||||
|
||||
/// Returns if the node should be processed by the preorder traversal (and
|
||||
/// then by the post-order one).
|
||||
///
|
||||
/// Note that this is true unconditionally for servo, since it requires to
|
||||
/// bubble the widths bottom-up for all the DOM.
|
||||
fn should_process(&self, node: N) -> bool {
|
||||
node.is_dirty() || node.has_dirty_descendants()
|
||||
}
|
||||
|
||||
/// Do an action over the child before pushing him to the work queue.
|
||||
///
|
||||
/// By default, propagate the IS_DIRTY flag down the tree.
|
||||
#[allow(unsafe_code)]
|
||||
fn pre_process_child_hook(&self, parent: N, kid: N) {
|
||||
// NOTE: At this point is completely safe to modify either the parent or
|
||||
// the child, since we have exclusive access to both of them.
|
||||
if parent.is_dirty() {
|
||||
unsafe {
|
||||
kid.set_dirty(true);
|
||||
parent.set_dirty_descendants(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculates the style for a single node.
|
||||
|
@ -244,22 +268,17 @@ pub fn recalc_style_at<'a, N, C>(context: &'a C,
|
|||
// NB: flow construction updates the bloom filter on the way up.
|
||||
put_thread_local_bloom_filter(bf, &unsafe_layout_node, context.shared_context());
|
||||
|
||||
// Mark the node as DIRTY_ON_VIEWPORT_SIZE_CHANGE is it uses viewport percentage units.
|
||||
match node.as_element() {
|
||||
Some(element) => {
|
||||
match *element.style_attribute() {
|
||||
Some(ref property_declaration_block) => {
|
||||
if property_declaration_block.declarations().any(|d| d.0.has_viewport_percentage()) {
|
||||
unsafe {
|
||||
node.set_dirty_on_viewport_size_changed();
|
||||
}
|
||||
node.set_descendants_dirty_on_viewport_size_changed();
|
||||
// Mark the node as DIRTY_ON_VIEWPORT_SIZE_CHANGE is it uses viewport
|
||||
// percentage units.
|
||||
if !node.needs_dirty_on_viewport_size_changed() {
|
||||
if let Some(element) = node.as_element() {
|
||||
if let Some(ref property_declaration_block) = *element.style_attribute() {
|
||||
if property_declaration_block.declarations().any(|d| d.0.has_viewport_percentage()) {
|
||||
unsafe {
|
||||
node.set_dirty_on_viewport_size_changed();
|
||||
}
|
||||
},
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
},
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue