mirror of
https://github.com/servo/servo.git
synced 2025-08-12 08:55:32 +01:00
style: Move all the style sharing code outside matching.rs
This is just a code health change. I want to move it away to keep matching.rs as simple as possible.
This commit is contained in:
parent
d98abe1397
commit
311c403522
6 changed files with 585 additions and 499 deletions
141
components/style/sharing/checks.rs
Normal file
141
components/style/sharing/checks.rs
Normal file
|
@ -0,0 +1,141 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
//! Different checks done during the style sharing process in order to determine
|
||||
//! quickly whether it's worth to share style, and whether two different
|
||||
//! elements can indeed share the same style.
|
||||
|
||||
use context::{CurrentElementInfo, SelectorFlagsMap, SharedStyleContext};
|
||||
use dom::TElement;
|
||||
use matching::MatchMethods;
|
||||
use selectors::bloom::BloomFilter;
|
||||
use selectors::matching::{ElementSelectorFlags, StyleRelations};
|
||||
use sharing::StyleSharingCandidate;
|
||||
use sink::ForgetfulSink;
|
||||
use stylearc::Arc;
|
||||
|
||||
/// Determines, based on the results of selector matching, whether it's worth to
|
||||
/// try to share style with this element, that is, to try to insert the element
|
||||
/// in the chache.
|
||||
#[inline]
|
||||
pub fn relations_are_shareable(relations: &StyleRelations) -> bool {
|
||||
use selectors::matching::*;
|
||||
!relations.intersects(AFFECTED_BY_ID_SELECTOR |
|
||||
AFFECTED_BY_PSEUDO_ELEMENTS |
|
||||
AFFECTED_BY_STYLE_ATTRIBUTE |
|
||||
AFFECTED_BY_PRESENTATIONAL_HINTS)
|
||||
}
|
||||
|
||||
/// Whether, given two elements, they have pointer-equal computed values.
|
||||
///
|
||||
/// Both elements need to be styled already.
|
||||
///
|
||||
/// This is used to know whether we can share style across cousins (if the two
|
||||
/// parents have the same style).
|
||||
pub fn same_computed_values<E>(first: Option<E>, second: Option<E>) -> bool
|
||||
where E: TElement,
|
||||
{
|
||||
let (a, b) = match (first, second) {
|
||||
(Some(f), Some(s)) => (f, s),
|
||||
_ => return false,
|
||||
};
|
||||
|
||||
let eq = Arc::ptr_eq(a.borrow_data().unwrap().styles().primary.values(),
|
||||
b.borrow_data().unwrap().styles().primary.values());
|
||||
eq
|
||||
}
|
||||
|
||||
/// Whether a given element has presentational hints.
|
||||
///
|
||||
/// We consider not worth to share style with an element that has presentational
|
||||
/// hints, both because implementing the code that compares that the hints are
|
||||
/// equal is somewhat annoying, and also because it'd be expensive enough.
|
||||
pub fn has_presentational_hints<E>(element: E) -> bool
|
||||
where E: TElement,
|
||||
{
|
||||
let mut hints = ForgetfulSink::new();
|
||||
element.synthesize_presentational_hints_for_legacy_attributes(&mut hints);
|
||||
!hints.is_empty()
|
||||
}
|
||||
|
||||
/// Whether a given element has the same class attribute than a given candidate.
|
||||
///
|
||||
/// We don't try to share style across elements with different class attributes.
|
||||
pub fn have_same_class<E>(element: E,
|
||||
candidate: &mut StyleSharingCandidate<E>)
|
||||
-> bool
|
||||
where E: TElement,
|
||||
{
|
||||
// XXX Efficiency here, I'm only validating ideas.
|
||||
let mut element_class_attributes = vec![];
|
||||
element.each_class(|c| element_class_attributes.push(c.clone()));
|
||||
|
||||
if candidate.class_attributes.is_none() {
|
||||
let mut attrs = vec![];
|
||||
candidate.element.each_class(|c| attrs.push(c.clone()));
|
||||
candidate.class_attributes = Some(attrs)
|
||||
}
|
||||
|
||||
element_class_attributes == *candidate.class_attributes.as_ref().unwrap()
|
||||
}
|
||||
|
||||
/// Whether a given element and a candidate match the same set of "revalidation"
|
||||
/// selectors.
|
||||
///
|
||||
/// Revalidation selectors are those that depend on the DOM structure, like
|
||||
/// :first-child, etc, or on attributes that we don't check off-hand (pretty
|
||||
/// much every attribute selector except `id` and `class`.
|
||||
#[inline]
|
||||
pub fn revalidate<E>(element: E,
|
||||
candidate: &mut StyleSharingCandidate<E>,
|
||||
shared_context: &SharedStyleContext,
|
||||
bloom: &BloomFilter,
|
||||
info: &mut CurrentElementInfo,
|
||||
selector_flags_map: &mut SelectorFlagsMap<E>)
|
||||
-> bool
|
||||
where E: TElement,
|
||||
{
|
||||
let stylist = &shared_context.stylist;
|
||||
|
||||
if info.revalidation_match_results.is_none() {
|
||||
// It's important to set the selector flags. Otherwise, if we succeed in
|
||||
// sharing the style, we may not set the slow selector flags for the
|
||||
// right elements (which may not necessarily be |element|), causing
|
||||
// missed restyles after future DOM mutations.
|
||||
//
|
||||
// Gecko's test_bug534804.html exercises this. A minimal testcase is:
|
||||
// <style> #e:empty + span { ... } </style>
|
||||
// <span id="e">
|
||||
// <span></span>
|
||||
// </span>
|
||||
// <span></span>
|
||||
//
|
||||
// The style sharing cache will get a hit for the second span. When the
|
||||
// child span is subsequently removed from the DOM, missing selector
|
||||
// flags would cause us to miss the restyle on the second span.
|
||||
let mut set_selector_flags = |el: &E, flags: ElementSelectorFlags| {
|
||||
element.apply_selector_flags(selector_flags_map, el, flags);
|
||||
};
|
||||
info.revalidation_match_results =
|
||||
Some(stylist.match_revalidation_selectors(&element, bloom,
|
||||
&mut set_selector_flags));
|
||||
}
|
||||
|
||||
if candidate.revalidation_match_results.is_none() {
|
||||
let results =
|
||||
stylist.match_revalidation_selectors(&*candidate.element, bloom,
|
||||
&mut |_, _| {});
|
||||
candidate.revalidation_match_results = Some(results);
|
||||
}
|
||||
|
||||
let for_element = info.revalidation_match_results.as_ref().unwrap();
|
||||
let for_candidate = candidate.revalidation_match_results.as_ref().unwrap();
|
||||
|
||||
// This assert "ensures", to some extent, that the two candidates have
|
||||
// matched the same rulehash buckets, and as such, that the bits we're
|
||||
// comparing represent the same set of selectors.
|
||||
debug_assert_eq!(for_element.len(), for_candidate.len());
|
||||
|
||||
for_element == for_candidate
|
||||
}
|
384
components/style/sharing/mod.rs
Normal file
384
components/style/sharing/mod.rs
Normal file
|
@ -0,0 +1,384 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
//! Code related to the style sharing cache, an optimization that allows similar
|
||||
//! nodes to share style without having to run selector matching twice.
|
||||
|
||||
use Atom;
|
||||
use bit_vec::BitVec;
|
||||
use cache::{LRUCache, LRUCacheMutIterator};
|
||||
use context::{CurrentElementInfo, SelectorFlagsMap, SharedStyleContext};
|
||||
use data::{ComputedStyle, ElementData, ElementStyles};
|
||||
use dom::{TElement, SendElement};
|
||||
use matching::{ChildCascadeRequirement, MatchMethods};
|
||||
use properties::ComputedValues;
|
||||
use selectors::bloom::BloomFilter;
|
||||
use selectors::matching::StyleRelations;
|
||||
use sink::ForgetfulSink;
|
||||
|
||||
mod checks;
|
||||
|
||||
/// The amount of nodes that the style sharing candidate cache should hold at
|
||||
/// most.
|
||||
pub const STYLE_SHARING_CANDIDATE_CACHE_SIZE: usize = 8;
|
||||
|
||||
/// Controls whether the style sharing cache is used.
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub enum StyleSharingBehavior {
|
||||
/// Style sharing allowed.
|
||||
Allow,
|
||||
/// Style sharing disallowed.
|
||||
Disallow,
|
||||
}
|
||||
|
||||
/// Information regarding a style sharing candidate, that is, an entry in the
|
||||
/// style sharing cache.
|
||||
///
|
||||
/// Note that this information is stored in TLS and cleared after the traversal,
|
||||
/// and once here, the style information of the element is immutable, so it's
|
||||
/// safe to access.
|
||||
///
|
||||
/// TODO: We can stick a lot more info here.
|
||||
#[derive(Debug)]
|
||||
pub struct StyleSharingCandidate<E: TElement> {
|
||||
/// The element. We use SendElement here so that the cache may live in
|
||||
/// ScopedTLS.
|
||||
element: SendElement<E>,
|
||||
/// The cached class names.
|
||||
class_attributes: Option<Vec<Atom>>,
|
||||
/// The cached result of matching this entry against the revalidation selectors.
|
||||
revalidation_match_results: Option<BitVec>,
|
||||
}
|
||||
|
||||
impl<E: TElement> PartialEq<StyleSharingCandidate<E>> for StyleSharingCandidate<E> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.element == other.element
|
||||
}
|
||||
}
|
||||
|
||||
/// A cache miss result.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum CacheMiss {
|
||||
/// The parents don't match.
|
||||
Parent,
|
||||
/// One element was NAC, while the other wasn't.
|
||||
NativeAnonymousContent,
|
||||
/// The local name of the element and the candidate don't match.
|
||||
LocalName,
|
||||
/// The namespace of the element and the candidate don't match.
|
||||
Namespace,
|
||||
/// One of the element or the candidate was a link, but the other one
|
||||
/// wasn't.
|
||||
Link,
|
||||
/// The element and the candidate match different kind of rules. This can
|
||||
/// only happen in Gecko.
|
||||
UserAndAuthorRules,
|
||||
/// The element and the candidate are in a different state.
|
||||
State,
|
||||
/// The element had an id attribute, which qualifies for a unique style.
|
||||
IdAttr,
|
||||
/// The element had a style attribute, which qualifies for a unique style.
|
||||
StyleAttr,
|
||||
/// The element and the candidate class names didn't match.
|
||||
Class,
|
||||
/// The presentation hints didn't match.
|
||||
PresHints,
|
||||
/// The element and the candidate didn't match the same set of revalidation
|
||||
/// selectors.
|
||||
Revalidation,
|
||||
}
|
||||
|
||||
/// The results of attempting to share a style.
|
||||
pub enum StyleSharingResult {
|
||||
/// We didn't find anybody to share the style with.
|
||||
CannotShare,
|
||||
/// The node's style can be shared. The integer specifies the index in the
|
||||
/// LRU cache that was hit and the damage that was done. The
|
||||
/// `ChildCascadeRequirement` indicates whether style changes due to using
|
||||
/// the shared style mean we need to recascade to children.
|
||||
StyleWasShared(usize, ChildCascadeRequirement),
|
||||
}
|
||||
|
||||
/// An LRU cache of the last few nodes seen, so that we can aggressively try to
|
||||
/// reuse their styles.
|
||||
///
|
||||
/// Note that this cache is flushed every time we steal work from the queue, so
|
||||
/// storing nodes here temporarily is safe.
|
||||
pub struct StyleSharingCandidateCache<E: TElement> {
|
||||
cache: LRUCache<StyleSharingCandidate<E>>,
|
||||
}
|
||||
|
||||
impl<E: TElement> StyleSharingCandidateCache<E> {
|
||||
/// Create a new style sharing candidate cache.
|
||||
pub fn new() -> Self {
|
||||
StyleSharingCandidateCache {
|
||||
cache: LRUCache::new(STYLE_SHARING_CANDIDATE_CACHE_SIZE),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the number of entries in the cache.
|
||||
pub fn num_entries(&self) -> usize {
|
||||
self.cache.num_entries()
|
||||
}
|
||||
|
||||
fn iter_mut(&mut self) -> LRUCacheMutIterator<StyleSharingCandidate<E>> {
|
||||
self.cache.iter_mut()
|
||||
}
|
||||
|
||||
/// Tries to insert an element in the style sharing cache.
|
||||
///
|
||||
/// Fails if we know it should never be in the cache.
|
||||
pub fn insert_if_possible(&mut self,
|
||||
element: &E,
|
||||
style: &ComputedValues,
|
||||
relations: StyleRelations,
|
||||
revalidation_match_results: Option<BitVec>) {
|
||||
let parent = match element.parent_element() {
|
||||
Some(element) => element,
|
||||
None => {
|
||||
debug!("Failing to insert to the cache: no parent element");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if element.is_native_anonymous() {
|
||||
debug!("Failing to insert into the cache: NAC");
|
||||
return;
|
||||
}
|
||||
|
||||
// These are things we don't check in the candidate match because they
|
||||
// are either uncommon or expensive.
|
||||
if !checks::relations_are_shareable(&relations) {
|
||||
debug!("Failing to insert to the cache: {:?}", relations);
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure we noted any presentational hints in the StyleRelations.
|
||||
if cfg!(debug_assertions) {
|
||||
let mut hints = ForgetfulSink::new();
|
||||
element.synthesize_presentational_hints_for_legacy_attributes(&mut hints);
|
||||
debug_assert!(hints.is_empty(),
|
||||
"Style relations should not be shareable!");
|
||||
}
|
||||
|
||||
let box_style = style.get_box();
|
||||
if box_style.specifies_transitions() {
|
||||
debug!("Failing to insert to the cache: transitions");
|
||||
return;
|
||||
}
|
||||
|
||||
if box_style.specifies_animations() {
|
||||
debug!("Failing to insert to the cache: animations");
|
||||
return;
|
||||
}
|
||||
|
||||
debug!("Inserting into cache: {:?} with parent {:?}", element, parent);
|
||||
|
||||
self.cache.insert(StyleSharingCandidate {
|
||||
element: unsafe { SendElement::new(*element) },
|
||||
class_attributes: None,
|
||||
revalidation_match_results: revalidation_match_results,
|
||||
});
|
||||
}
|
||||
|
||||
/// Touch a given index in the style sharing candidate cache.
|
||||
pub fn touch(&mut self, index: usize) {
|
||||
self.cache.touch(index);
|
||||
}
|
||||
|
||||
/// Clear the style sharing candidate cache.
|
||||
pub fn clear(&mut self) {
|
||||
self.cache.evict_all()
|
||||
}
|
||||
|
||||
/// Attempts to share a style with another node.
|
||||
///
|
||||
/// This method is unsafe because it depends on the
|
||||
/// `style_sharing_candidate_cache` having only live nodes in it, and we
|
||||
/// have no way to guarantee that at the type system level yet.
|
||||
pub unsafe fn share_style_if_possible(
|
||||
&mut self,
|
||||
shared_context: &SharedStyleContext,
|
||||
current_element_info: &mut CurrentElementInfo,
|
||||
selector_flags_map: &mut SelectorFlagsMap<E>,
|
||||
bloom_filter: &BloomFilter,
|
||||
element: E,
|
||||
data: &mut ElementData
|
||||
) -> StyleSharingResult {
|
||||
if shared_context.options.disable_style_sharing_cache {
|
||||
debug!("{:?} Cannot share style: style sharing cache disabled",
|
||||
element);
|
||||
return StyleSharingResult::CannotShare
|
||||
}
|
||||
|
||||
if element.parent_element().is_none() {
|
||||
debug!("{:?} Cannot share style: element has no parent", element);
|
||||
return StyleSharingResult::CannotShare
|
||||
}
|
||||
|
||||
if element.is_native_anonymous() {
|
||||
debug!("{:?} Cannot share style: NAC", element);
|
||||
return StyleSharingResult::CannotShare;
|
||||
}
|
||||
|
||||
if element.style_attribute().is_some() {
|
||||
debug!("{:?} Cannot share style: element has style attribute",
|
||||
element);
|
||||
return StyleSharingResult::CannotShare
|
||||
}
|
||||
|
||||
if element.get_id().is_some() {
|
||||
debug!("{:?} Cannot share style: element has id", element);
|
||||
return StyleSharingResult::CannotShare
|
||||
}
|
||||
|
||||
let mut should_clear_cache = false;
|
||||
for (i, candidate) in self.iter_mut().enumerate() {
|
||||
let sharing_result =
|
||||
Self::test_candidate(
|
||||
element,
|
||||
candidate,
|
||||
&shared_context,
|
||||
bloom_filter,
|
||||
current_element_info,
|
||||
selector_flags_map
|
||||
);
|
||||
|
||||
match sharing_result {
|
||||
Ok(shared_style) => {
|
||||
// Yay, cache hit. Share the style.
|
||||
|
||||
// Accumulate restyle damage.
|
||||
debug_assert_eq!(data.has_styles(), data.has_restyle());
|
||||
let old_values = data.get_styles_mut()
|
||||
.and_then(|s| s.primary.values.take());
|
||||
let child_cascade_requirement =
|
||||
element.accumulate_damage(
|
||||
&shared_context,
|
||||
data.get_restyle_mut(),
|
||||
old_values.as_ref().map(|v| &**v),
|
||||
shared_style.values(),
|
||||
None
|
||||
);
|
||||
|
||||
// We never put elements with pseudo style into the style
|
||||
// sharing cache, so we can just mint an ElementStyles
|
||||
// directly here.
|
||||
//
|
||||
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1329361
|
||||
let styles = ElementStyles::new(shared_style);
|
||||
data.set_styles(styles);
|
||||
|
||||
return StyleSharingResult::StyleWasShared(i, child_cascade_requirement)
|
||||
}
|
||||
Err(miss) => {
|
||||
debug!("Cache miss: {:?}", miss);
|
||||
|
||||
// Cache miss, let's see what kind of failure to decide
|
||||
// whether we keep trying or not.
|
||||
match miss {
|
||||
// Cache miss because of parent, clear the candidate cache.
|
||||
CacheMiss::Parent => {
|
||||
should_clear_cache = true;
|
||||
break;
|
||||
},
|
||||
// Too expensive failure, give up, we don't want another
|
||||
// one of these.
|
||||
CacheMiss::PresHints |
|
||||
CacheMiss::Revalidation => break,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
debug!("{:?} Cannot share style: {} cache entries", element,
|
||||
self.cache.num_entries());
|
||||
|
||||
if should_clear_cache {
|
||||
self.clear();
|
||||
}
|
||||
|
||||
StyleSharingResult::CannotShare
|
||||
}
|
||||
|
||||
fn test_candidate(element: E,
|
||||
candidate: &mut StyleSharingCandidate<E>,
|
||||
shared: &SharedStyleContext,
|
||||
bloom: &BloomFilter,
|
||||
info: &mut CurrentElementInfo,
|
||||
selector_flags_map: &mut SelectorFlagsMap<E>)
|
||||
-> Result<ComputedStyle, CacheMiss> {
|
||||
macro_rules! miss {
|
||||
($miss: ident) => {
|
||||
return Err(CacheMiss::$miss);
|
||||
}
|
||||
}
|
||||
|
||||
// Check that we have the same parent, or at least the same pointer
|
||||
// identity for parent computed style. The latter check allows us to
|
||||
// share style between cousins if the parents shared style.
|
||||
let parent = element.parent_element();
|
||||
let candidate_parent = candidate.element.parent_element();
|
||||
if parent != candidate_parent &&
|
||||
!checks::same_computed_values(parent, candidate_parent) {
|
||||
miss!(Parent)
|
||||
}
|
||||
|
||||
if element.is_native_anonymous() {
|
||||
debug_assert!(!candidate.element.is_native_anonymous(),
|
||||
"Why inserting NAC into the cache?");
|
||||
miss!(NativeAnonymousContent)
|
||||
}
|
||||
|
||||
if *element.get_local_name() != *candidate.element.get_local_name() {
|
||||
miss!(LocalName)
|
||||
}
|
||||
|
||||
if *element.get_namespace() != *candidate.element.get_namespace() {
|
||||
miss!(Namespace)
|
||||
}
|
||||
|
||||
if element.is_link() != candidate.element.is_link() {
|
||||
miss!(Link)
|
||||
}
|
||||
|
||||
if element.matches_user_and_author_rules() !=
|
||||
candidate.element.matches_user_and_author_rules() {
|
||||
miss!(UserAndAuthorRules)
|
||||
}
|
||||
|
||||
if element.get_state() != candidate.element.get_state() {
|
||||
miss!(State)
|
||||
}
|
||||
|
||||
if element.get_id() != candidate.element.get_id() {
|
||||
miss!(IdAttr)
|
||||
}
|
||||
|
||||
if element.style_attribute().is_some() {
|
||||
miss!(StyleAttr)
|
||||
}
|
||||
|
||||
if !checks::have_same_class(element, candidate) {
|
||||
miss!(Class)
|
||||
}
|
||||
|
||||
if checks::has_presentational_hints(element) {
|
||||
miss!(PresHints)
|
||||
}
|
||||
|
||||
if !checks::revalidate(element, candidate, shared, bloom, info,
|
||||
selector_flags_map) {
|
||||
miss!(Revalidation)
|
||||
}
|
||||
|
||||
let data = candidate.element.borrow_data().unwrap();
|
||||
debug_assert!(element.has_current_styles(&data));
|
||||
|
||||
debug!("Sharing style between {:?} and {:?}",
|
||||
element, candidate.element);
|
||||
Ok(data.styles().primary.clone())
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue