Incremental Style Recalc

This patch puts in the initial framework for incremental reflow. Nodes' styles
are no longer recalculated unless the node has changed.

I've been hacking on the general problem of incremental reflow for the past
couple weeks, and I've yet to get a full implementation that actually passes all
the reftests + wikipedia + cnn. Therefore, I'm going to try to land the different
parts of it one by one.

This patch only does incremental style recalc, without incremental flow
construction, inline-size bubbling, reflow, or display lists. Those will be coming
in that order as I finish them.

At least with this strategy, I can land a working version of incremental reflow,
even if not yet complete.

r? @pcwalton
This commit is contained in:
Clark Gaebel 2014-10-06 11:40:05 -04:00
parent 510f8a817f
commit d12c6e7383
31 changed files with 641 additions and 424 deletions

View file

@ -49,8 +49,10 @@ use http::headers::response::HeaderCollection as ResponseHeaderCollection;
use http::headers::request::HeaderCollection as RequestHeaderCollection;
use http::method::Method;
use std::io::timer::Timer;
use script_traits::UntrustedNodeAddress;
use servo_msg::compositor_msg::ScriptListener;
use servo_msg::constellation_msg::ConstellationChan;
use servo_util::smallvec::{SmallVec1, SmallVec};
use layout_interface::{LayoutRPC, LayoutChan};
use dom::bindings::utils::WindowProxyHandler;
@ -148,6 +150,17 @@ impl<T: JSTraceable> JSTraceable for Vec<T> {
}
}
// XXXManishearth Check if the following three are optimized to no-ops
// if e.trace() is a no-op (e.g it is an untraceable type)
impl<T: JSTraceable + 'static> JSTraceable for SmallVec1<T> {
#[inline]
fn trace(&self, trc: *mut JSTracer) {
for e in self.iter() {
e.trace(trc);
}
}
}
impl<T: JSTraceable> JSTraceable for Option<T> {
#[inline]
fn trace(&self, trc: *mut JSTracer) {
@ -192,6 +205,7 @@ untraceable!(ResponseHeaderCollection, RequestHeaderCollection, Method)
untraceable!(ConstellationChan)
untraceable!(LayoutChan)
untraceable!(WindowProxyHandler)
untraceable!(UntrustedNodeAddress)
impl<'a> JSTraceable for &'a str {
#[inline]

View file

@ -54,7 +54,6 @@ use dom::uievent::UIEvent;
use dom::window::{Window, WindowHelpers};
use html::hubbub_html_parser::build_element_from_tag;
use hubbub::hubbub::{QuirksMode, NoQuirks, LimitedQuirks, FullQuirks};
use layout_interface::{DocumentDamageLevel, ContentChangedDocumentDamage};
use servo_util::namespace;
use servo_util::str::{DOMString, split_html_space_chars};
@ -165,8 +164,8 @@ pub trait DocumentHelpers<'a> {
fn set_quirks_mode(self, mode: QuirksMode);
fn set_last_modified(self, value: DOMString);
fn set_encoding_name(self, name: DOMString);
fn content_changed(self);
fn damage_and_reflow(self, damage: DocumentDamageLevel);
fn content_changed(self, node: JSRef<Node>);
fn reflow(self);
fn wait_until_safe_to_modify_dom(self);
fn unregister_named_element(self, to_unregister: JSRef<Element>, id: Atom);
fn register_named_element(self, element: JSRef<Element>, id: Atom);
@ -195,19 +194,19 @@ impl<'a> DocumentHelpers<'a> for JSRef<'a, Document> {
*self.encoding_name.borrow_mut() = name;
}
fn content_changed(self) {
self.damage_and_reflow(ContentChangedDocumentDamage);
fn content_changed(self, node: JSRef<Node>) {
node.dirty();
self.reflow();
}
fn damage_and_reflow(self, damage: DocumentDamageLevel) {
self.window.root().damage_and_reflow(damage);
fn reflow(self) {
self.window.root().reflow();
}
fn wait_until_safe_to_modify_dom(self) {
self.window.root().wait_until_safe_to_modify_dom();
}
/// Remove any existing association between the provided id and any elements in this document.
fn unregister_named_element(self,
to_unregister: JSRef<Element>,

View file

@ -28,8 +28,6 @@ use dom::node::{ElementNodeTypeId, Node, NodeHelpers, NodeIterator, document_fro
use dom::node::{window_from_node, LayoutNodeHelpers};
use dom::nodelist::NodeList;
use dom::virtualmethods::{VirtualMethods, vtable_for};
use layout_interface::ContentChangedDocumentDamage;
use layout_interface::MatchSelectorsDocumentDamage;
use devtools_traits::AttrInfo;
use style::{matches, parse_selector_list_from_str};
use style;
@ -323,6 +321,7 @@ pub trait AttributeHandlers {
fn remove_attribute(self, namespace: Namespace, name: &str);
fn notify_attribute_changed(self, local_name: &Atom);
fn has_class(&self, name: &str) -> bool;
fn notify_attribute_removed(self);
fn set_atomic_attribute(self, name: &str, value: DOMString);
@ -436,19 +435,24 @@ impl<'a> AttributeHandlers for JSRef<'a, Element> {
}
self.attrs.borrow_mut().remove(idx);
self.notify_attribute_removed();
}
};
}
fn notify_attribute_changed(self, local_name: &Atom) {
fn notify_attribute_changed(self, _local_name: &Atom) {
let node: JSRef<Node> = NodeCast::from_ref(self);
if node.is_in_doc() {
let damage = match local_name.as_slice() {
"style" | "id" | "class" => MatchSelectorsDocumentDamage,
_ => ContentChangedDocumentDamage
};
let document = node.owner_doc().root();
document.damage_and_reflow(damage);
document.content_changed(node);
}
}
fn notify_attribute_removed(self) {
let node: JSRef<Node> = NodeCast::from_ref(self);
if node.is_in_doc() {
let document = node.owner_doc().root();
document.content_changed(node);
}
}

View file

@ -175,7 +175,8 @@ fn broadcast_radio_checked(broadcaster: JSRef<HTMLInputElement>, group: Option<&
impl<'a> HTMLInputElementHelpers for JSRef<'a, HTMLInputElement> {
fn force_relayout(self) {
let doc = document_from_node(self).root();
doc.content_changed()
let node: JSRef<Node> = NodeCast::from_ref(self);
doc.content_changed(node)
}
fn radio_group_updated(self, group: Option<&str>) {

View file

@ -46,8 +46,9 @@ use dom::window::Window;
use geom::rect::Rect;
use html::hubbub_html_parser::build_element_from_tag;
use layout_interface::{ContentBoxResponse, ContentBoxesResponse, LayoutRPC,
LayoutChan, ReapLayoutDataMsg, TrustedNodeAddress, UntrustedNodeAddress};
LayoutChan, ReapLayoutDataMsg, TrustedNodeAddress};
use devtools_traits::NodeInfo;
use script_traits::UntrustedNodeAddress;
use servo_util::geometry::Au;
use servo_util::str::{DOMString, null_str_as_empty};
use style::{parse_selector_list_from_str, matches};
@ -56,7 +57,7 @@ use js::jsapi::{JSContext, JSObject, JSTracer, JSRuntime};
use js::jsfriendapi;
use libc;
use libc::uintptr_t;
use std::cell::{RefCell, Ref, RefMut};
use std::cell::{Cell, RefCell, Ref, RefMut};
use std::default::Default;
use std::iter::{Map, Filter};
use std::mem;
@ -101,7 +102,7 @@ pub struct Node {
child_list: MutNullableJS<NodeList>,
/// A bitfield of flags for node items.
flags: RefCell<NodeFlags>,
flags: Cell<NodeFlags>,
/// Layout information. Only the layout task may touch this data.
///
@ -132,14 +133,19 @@ bitflags! {
#[doc = "Specifies whether this node is in disabled state."]
static InDisabledState = 0x04,
#[doc = "Specifies whether this node is in enabled state."]
static InEnabledState = 0x08
static InEnabledState = 0x08,
#[doc = "Specifies whether this node has changed since the last reflow."]
static IsDirty = 0x10,
#[doc = "Specifies whether this node has descendants (inclusive of itself) which \
have changed since the last reflow."]
static HasDirtyDescendants = 0x20,
}
}
impl NodeFlags {
pub fn new(type_id: NodeTypeId) -> NodeFlags {
match type_id {
DocumentNodeTypeId => IsInDoc,
DocumentNodeTypeId => IsInDoc | IsDirty,
// The following elements are enabled by default.
ElementNodeTypeId(HTMLButtonElementTypeId) |
ElementNodeTypeId(HTMLInputElementTypeId) |
@ -148,8 +154,8 @@ impl NodeFlags {
ElementNodeTypeId(HTMLOptGroupElementTypeId) |
ElementNodeTypeId(HTMLOptionElementTypeId) |
//ElementNodeTypeId(HTMLMenuItemElementTypeId) |
ElementNodeTypeId(HTMLFieldSetElementTypeId) => InEnabledState,
_ => NodeFlags::empty(),
ElementNodeTypeId(HTMLFieldSetElementTypeId) => InEnabledState | IsDirty,
_ => IsDirty,
}
}
}
@ -271,7 +277,7 @@ impl<'a> PrivateNodeHelpers for JSRef<'a, Node> {
let parent = self.parent_node().root();
parent.map(|parent| vtable_for(&*parent).child_inserted(self));
document.content_changed();
document.content_changed(self);
}
// http://dom.spec.whatwg.org/#node-is-removed
@ -283,7 +289,7 @@ impl<'a> PrivateNodeHelpers for JSRef<'a, Node> {
vtable_for(&node).unbind_from_tree(parent_in_doc);
}
document.content_changed();
document.content_changed(self);
}
//
@ -395,6 +401,9 @@ pub trait NodeHelpers<'a> {
fn is_text(self) -> bool;
fn is_anchor_element(self) -> bool;
fn get_flag(self, flag: NodeFlags) -> bool;
fn set_flag(self, flag: NodeFlags, value: bool);
fn get_hover_state(self) -> bool;
fn set_hover_state(self, state: bool);
@ -404,6 +413,17 @@ pub trait NodeHelpers<'a> {
fn get_enabled_state(self) -> bool;
fn set_enabled_state(self, state: bool);
fn get_is_dirty(self) -> bool;
fn set_is_dirty(self, state: bool);
fn get_has_dirty_descendants(self) -> bool;
fn set_has_dirty_descendants(self, state: bool);
/// Marks the given node as `IsDirty`, its siblings as `IsDirty` (to deal
/// with sibling selectors), its ancestors as `HasDirtyDescendants`, and its
/// descendants as `IsDirty`.
fn dirty(self);
fn dump(self);
fn dump_indent(self, indent: uint);
fn debug_str(self) -> String;
@ -426,6 +446,7 @@ pub trait NodeHelpers<'a> {
fn summarize(self) -> NodeInfo;
}
impl<'a> NodeHelpers<'a> for JSRef<'a, Node> {
/// Dumps the subtree rooted at this node, for debugging.
fn dump(self) {
@ -454,7 +475,7 @@ impl<'a> NodeHelpers<'a> for JSRef<'a, Node> {
}
fn is_in_doc(self) -> bool {
self.flags.borrow().contains(IsInDoc)
self.deref().flags.get().contains(IsInDoc)
}
/// Returns the type ID of this node. Fails if this node is borrowed mutably.
@ -512,39 +533,98 @@ impl<'a> NodeHelpers<'a> for JSRef<'a, Node> {
self.type_id == TextNodeTypeId
}
fn get_flag(self, flag: NodeFlags) -> bool {
self.flags.get().contains(flag)
}
fn set_flag(self, flag: NodeFlags, value: bool) {
let mut flags = self.flags.get();
if value {
flags.insert(flag);
} else {
flags.remove(flag);
}
self.flags.set(flags);
}
fn get_hover_state(self) -> bool {
self.flags.borrow().contains(InHoverState)
self.get_flag(InHoverState)
}
fn set_hover_state(self, state: bool) {
if state {
self.flags.borrow_mut().insert(InHoverState);
} else {
self.flags.borrow_mut().remove(InHoverState);
}
self.set_flag(InHoverState, state)
}
fn get_disabled_state(self) -> bool {
self.flags.borrow().contains(InDisabledState)
self.get_flag(InDisabledState)
}
fn set_disabled_state(self, state: bool) {
if state {
self.flags.borrow_mut().insert(InDisabledState);
} else {
self.flags.borrow_mut().remove(InDisabledState);
}
self.set_flag(InDisabledState, state)
}
fn get_enabled_state(self) -> bool {
self.flags.borrow().contains(InEnabledState)
self.get_flag(InEnabledState)
}
fn set_enabled_state(self, state: bool) {
if state {
self.flags.borrow_mut().insert(InEnabledState);
} else {
self.flags.borrow_mut().remove(InEnabledState);
self.set_flag(InEnabledState, state)
}
fn get_is_dirty(self) -> bool {
self.get_flag(IsDirty)
}
fn set_is_dirty(self, state: bool) {
self.set_flag(IsDirty, state)
}
fn get_has_dirty_descendants(self) -> bool {
self.get_flag(HasDirtyDescendants)
}
fn set_has_dirty_descendants(self, state: bool) {
self.set_flag(HasDirtyDescendants, state)
}
fn dirty(self) {
// 1. Dirty descendants.
fn dirty_subtree(node: JSRef<Node>) {
node.set_is_dirty(true);
let mut has_dirty_descendants = false;
for kid in node.children() {
dirty_subtree(kid);
has_dirty_descendants = true;
}
if has_dirty_descendants {
node.set_has_dirty_descendants(true);
}
}
dirty_subtree(self);
// 2. Dirty siblings.
//
// TODO(cgaebel): This is a very conservative way to account for sibling
// selectors. Maybe we can do something smarter in the future.
let parent =
match self.parent_node() {
None => return,
Some(parent) => parent,
};
for sibling in parent.root().children() {
sibling.set_is_dirty(true);
}
// 3. Dirty ancestors.
for ancestor in self.ancestors() {
if ancestor.get_has_dirty_descendants() { break }
ancestor.set_has_dirty_descendants(true);
}
}
@ -734,6 +814,7 @@ impl<'a> NodeHelpers<'a> for JSRef<'a, Node> {
incompleteValue: false, //FIXME: reflect truncation
}
}
}
/// If the given untrusted node address represents a valid DOM node in the given runtime,
@ -764,6 +845,8 @@ pub trait LayoutNodeHelpers {
unsafe fn owner_doc_for_layout(&self) -> JS<Document>;
unsafe fn is_element_for_layout(&self) -> bool;
unsafe fn get_flag(self, flag: NodeFlags) -> bool;
unsafe fn set_flag(self, flag: NodeFlags, value: bool);
}
impl LayoutNodeHelpers for JS<Node> {
@ -806,6 +889,25 @@ impl LayoutNodeHelpers for JS<Node> {
unsafe fn owner_doc_for_layout(&self) -> JS<Document> {
(*self.unsafe_get()).owner_doc.get_inner().unwrap()
}
#[inline]
unsafe fn get_flag(self, flag: NodeFlags) -> bool {
(*self.unsafe_get()).flags.get().contains(flag)
}
#[inline]
unsafe fn set_flag(self, flag: NodeFlags, value: bool) {
let this = self.unsafe_get();
let mut flags = (*this).flags.get();
if value {
flags.insert(flag);
} else {
flags.remove(flag);
}
(*this).flags.set(flags);
}
}
pub trait RawLayoutNodeHelpers {
@ -1034,8 +1136,7 @@ impl Node {
prev_sibling: Default::default(),
owner_doc: MutNullableJS::new(doc),
child_list: Default::default(),
flags: RefCell::new(NodeFlags::new(type_id)),
flags: Cell::new(NodeFlags::new(type_id)),
layout_data: LayoutDataRef::new(),
@ -1236,11 +1337,13 @@ impl Node {
parent.add_child(*node, child);
let is_in_doc = parent.is_in_doc();
for kid in node.traverse_preorder() {
let mut flags = kid.flags.get();
if is_in_doc {
kid.flags.borrow_mut().insert(IsInDoc);
flags.insert(IsInDoc);
} else {
kid.flags.borrow_mut().remove(IsInDoc);
flags.remove(IsInDoc);
}
kid.flags.set(flags);
}
}
@ -1326,7 +1429,7 @@ impl Node {
// Step 8.
parent.remove_child(node);
node.flags.borrow_mut().remove(IsInDoc);
node.set_flag(IsInDoc, false);
// Step 9.
match suppress_observers {
@ -1660,7 +1763,7 @@ impl<'a> NodeMethods for JSRef<'a, Node> {
// Notify the document that the content of this node is different
let document = self.owner_doc().root();
document.content_changed();
document.content_changed(self);
}
DoctypeNodeTypeId |
DocumentNodeTypeId => {}
@ -2120,6 +2223,12 @@ impl<'a> style::TNode<'a, JSRef<'a, Element>> for JSRef<'a, Node> {
assert!(elem.is_some());
elem.unwrap().html_element_in_html_document()
}
fn is_dirty(self) -> bool { self.get_is_dirty() }
unsafe fn set_dirty(self, value: bool) { self.set_is_dirty(value) }
fn has_dirty_descendants(self) -> bool { self.get_has_dirty_descendants() }
unsafe fn set_dirty_descendants(self, value: bool) { self.set_has_dirty_descendants(value) }
}
pub trait DisabledStateHelpers {

View file

@ -18,7 +18,7 @@ use dom::location::Location;
use dom::navigator::Navigator;
use dom::performance::Performance;
use dom::screen::Screen;
use layout_interface::{ReflowGoal, DocumentDamageLevel};
use layout_interface::{ReflowGoal, ReflowForDisplay};
use page::Page;
use script_task::{ExitWindowMsg, FireTimerMsg, ScriptChan, TriggerLoadMsg, TriggerFragmentMsg};
use script_traits::ScriptControlChan;
@ -366,7 +366,7 @@ impl Reflectable for Window {
}
pub trait WindowHelpers {
fn damage_and_reflow(self, damage: DocumentDamageLevel);
fn reflow(self);
fn flush_layout(self, goal: ReflowGoal);
fn wait_until_safe_to_modify_dom(self);
fn init_browser_context(self, doc: JSRef<Document>);
@ -399,9 +399,12 @@ impl<'a> WindowHelpers for JSRef<'a, Window> {
})
}
fn damage_and_reflow(self, damage: DocumentDamageLevel) {
self.page().damage(damage);
self.page().avoided_reflows.set(self.page().avoided_reflows.get() + 1);
fn reflow(self) {
self.page().damage();
// FIXME This should probably be ReflowForQuery, not Display. All queries currently
// currently rely on the display list, which means we can't destroy it by
// doing a query reflow.
self.page().reflow(ReflowForDisplay, self.control_chan.clone(), &*self.compositor);
}
fn flush_layout(self, goal: ReflowGoal) {