diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
commit | 0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch) | |
tree | a31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /servo/components/style/bloom.rs | |
parent | Initial commit. (diff) | |
download | firefox-esr-upstream/115.8.0esr.tar.xz firefox-esr-upstream/115.8.0esr.zip |
Adding upstream version 115.8.0esr.upstream/115.8.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'servo/components/style/bloom.rs')
-rw-r--r-- | servo/components/style/bloom.rs | 400 |
1 files changed, 400 insertions, 0 deletions
diff --git a/servo/components/style/bloom.rs b/servo/components/style/bloom.rs new file mode 100644 index 0000000000..c111454392 --- /dev/null +++ b/servo/components/style/bloom.rs @@ -0,0 +1,400 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +//! The style bloom filter is used as an optimization when matching deep +//! descendant selectors. + +#![deny(missing_docs)] + +use crate::dom::{SendElement, TElement}; +use atomic_refcell::{AtomicRefCell, AtomicRefMut}; +use owning_ref::OwningHandle; +use selectors::bloom::BloomFilter; +use servo_arc::Arc; +use smallvec::SmallVec; +use std::mem::ManuallyDrop; + +thread_local! { + /// Bloom filters are large allocations, so we store them in thread-local storage + /// such that they can be reused across style traversals. StyleBloom is responsible + /// for ensuring that the bloom filter is zeroed when it is dropped. + /// + /// We intentionally leak this from TLS because we don't have the guarantee + /// of TLS destructors to run in worker threads. + /// + /// We could change this once https://github.com/rayon-rs/rayon/issues/688 + /// is fixed, hopefully. + static BLOOM_KEY: ManuallyDrop<Arc<AtomicRefCell<BloomFilter>>> = + ManuallyDrop::new(Arc::new_leaked(Default::default())); +} + +/// A struct that allows us to fast-reject deep descendant selectors avoiding +/// selector-matching. +/// +/// This is implemented using a counting bloom filter, and it's a standard +/// optimization. See Gecko's `AncestorFilter`, and Blink's and WebKit's +/// `SelectorFilter`. +/// +/// The constraints for Servo's style system are a bit different compared to +/// traditional style systems given Servo does a parallel breadth-first +/// traversal instead of a sequential depth-first traversal. +/// +/// This implies that we need to track a bit more state than other browsers to +/// ensure we're doing the correct thing during the traversal, and being able to +/// apply this optimization effectively. +/// +/// Concretely, we have a bloom filter instance per worker thread, and we track +/// the current DOM depth in order to find a common ancestor when it doesn't +/// match the previous element we've styled. +/// +/// This is usually a pretty fast operation (we use to be one level deeper than +/// the previous one), but in the case of work-stealing, we may needed to push +/// and pop multiple elements. +/// +/// See the `insert_parents_recovering`, where most of the magic happens. +/// +/// Regarding thread-safety, this struct is safe because: +/// +/// * We clear this after a restyle. +/// * The DOM shape and attributes (and every other thing we access here) are +/// immutable during a restyle. +/// +pub struct StyleBloom<E: TElement> { + /// A handle to the bloom filter from the thread upon which this StyleBloom + /// was created. We use AtomicRefCell so that this is all |Send|, which allows + /// StyleBloom to live in ThreadLocalStyleContext, which is dropped from the + /// parent thread. + filter: OwningHandle<Arc<AtomicRefCell<BloomFilter>>, AtomicRefMut<'static, BloomFilter>>, + + /// The stack of elements that this bloom filter contains, along with the + /// number of hashes pushed for each element. + elements: SmallVec<[PushedElement<E>; 16]>, + + /// Stack of hashes that have been pushed onto this filter. + pushed_hashes: SmallVec<[u32; 64]>, +} + +/// The very rough benchmarks in the selectors crate show clear() +/// costing about 25 times more than remove_hash(). We use this to implement +/// clear() more efficiently when only a small number of hashes have been +/// pushed. +/// +/// One subtly to note is that remove_hash() will not touch the value +/// if the filter overflowed. However, overflow can only occur if we +/// get 255 collisions on the same hash value, and 25 < 255. +const MEMSET_CLEAR_THRESHOLD: usize = 25; + +struct PushedElement<E: TElement> { + /// The element that was pushed. + element: SendElement<E>, + + /// The number of hashes pushed for the element. + num_hashes: usize, +} + +impl<E: TElement> PushedElement<E> { + fn new(el: E, num_hashes: usize) -> Self { + PushedElement { + element: unsafe { SendElement::new(el) }, + num_hashes, + } + } +} + +/// Returns whether the attribute name is excluded from the bloom filter. +/// +/// We do this for attributes that are very common but not commonly used in +/// selectors. +#[inline] +pub fn is_attr_name_excluded_from_filter(atom: &crate::Atom) -> bool { + *atom == atom!("class") || *atom == atom!("id") || *atom == atom!("style") +} + +fn each_relevant_element_hash<E, F>(element: E, mut f: F) +where + E: TElement, + F: FnMut(u32), +{ + f(element.local_name().get_hash()); + f(element.namespace().get_hash()); + + if let Some(id) = element.id() { + f(id.get_hash()); + } + + element.each_class(|class| f(class.get_hash())); + + element.each_attr_name(|name| { + if !is_attr_name_excluded_from_filter(name) { + f(name.get_hash()) + } + }); +} + +impl<E: TElement> Drop for StyleBloom<E> { + fn drop(&mut self) { + // Leave the reusable bloom filter in a zeroed state. + self.clear(); + } +} + +impl<E: TElement> StyleBloom<E> { + /// Create an empty `StyleBloom`. Because StyleBloom acquires the thread- + /// local filter buffer, creating multiple live StyleBloom instances at + /// the same time on the same thread will panic. + + // Forced out of line to limit stack frame sizes after extra inlining from + // https://github.com/rust-lang/rust/pull/43931 + // + // See https://github.com/servo/servo/pull/18420#issuecomment-328769322 + #[inline(never)] + pub fn new() -> Self { + let bloom_arc = BLOOM_KEY.with(|b| Arc::clone(&*b)); + let filter = + OwningHandle::new_with_fn(bloom_arc, |x| unsafe { x.as_ref() }.unwrap().borrow_mut()); + debug_assert!( + filter.is_zeroed(), + "Forgot to zero the bloom filter last time" + ); + StyleBloom { + filter, + elements: Default::default(), + pushed_hashes: Default::default(), + } + } + + /// Return the bloom filter used properly by the `selectors` crate. + pub fn filter(&self) -> &BloomFilter { + &*self.filter + } + + /// Push an element to the bloom filter, knowing that it's a child of the + /// last element parent. + pub fn push(&mut self, element: E) { + if cfg!(debug_assertions) { + if self.elements.is_empty() { + assert!(element.traversal_parent().is_none()); + } + } + self.push_internal(element); + } + + /// Same as `push`, but without asserting, in order to use it from + /// `rebuild`. + fn push_internal(&mut self, element: E) { + let mut count = 0; + each_relevant_element_hash(element, |hash| { + count += 1; + self.filter.insert_hash(hash); + self.pushed_hashes.push(hash); + }); + self.elements.push(PushedElement::new(element, count)); + } + + /// Pop the last element in the bloom filter and return it. + #[inline] + fn pop(&mut self) -> Option<E> { + let PushedElement { + element, + num_hashes, + } = self.elements.pop()?; + let popped_element = *element; + + // Verify that the pushed hashes match the ones we'd get from the element. + let mut expected_hashes = vec![]; + if cfg!(debug_assertions) { + each_relevant_element_hash(popped_element, |hash| expected_hashes.push(hash)); + } + + for _ in 0..num_hashes { + let hash = self.pushed_hashes.pop().unwrap(); + debug_assert_eq!(expected_hashes.pop().unwrap(), hash); + self.filter.remove_hash(hash); + } + + Some(popped_element) + } + + /// Returns the DOM depth of elements that can be correctly + /// matched against the bloom filter (that is, the number of + /// elements in our list). + pub fn matching_depth(&self) -> usize { + self.elements.len() + } + + /// Clears the bloom filter. + pub fn clear(&mut self) { + self.elements.clear(); + + if self.pushed_hashes.len() > MEMSET_CLEAR_THRESHOLD { + self.filter.clear(); + self.pushed_hashes.clear(); + } else { + for hash in self.pushed_hashes.drain(..) { + self.filter.remove_hash(hash); + } + debug_assert!(self.filter.is_zeroed()); + } + } + + /// Rebuilds the bloom filter up to the parent of the given element. + pub fn rebuild(&mut self, mut element: E) { + self.clear(); + + let mut parents_to_insert = SmallVec::<[E; 16]>::new(); + while let Some(parent) = element.traversal_parent() { + parents_to_insert.push(parent); + element = parent; + } + + for parent in parents_to_insert.drain(..).rev() { + self.push(parent); + } + } + + /// In debug builds, asserts that all the parents of `element` are in the + /// bloom filter. + /// + /// Goes away in release builds. + pub fn assert_complete(&self, mut element: E) { + if cfg!(debug_assertions) { + let mut checked = 0; + while let Some(parent) = element.traversal_parent() { + assert_eq!( + parent, + *(self.elements[self.elements.len() - 1 - checked].element) + ); + element = parent; + checked += 1; + } + assert_eq!(checked, self.elements.len()); + } + } + + /// Get the element that represents the chain of things inserted + /// into the filter right now. That chain is the given element + /// (if any) and its ancestors. + #[inline] + pub fn current_parent(&self) -> Option<E> { + self.elements.last().map(|ref el| *el.element) + } + + /// Insert the parents of an element in the bloom filter, trying to recover + /// the filter if the last element inserted doesn't match. + /// + /// Gets the element depth in the dom, to make it efficient, or if not + /// provided always rebuilds the filter from scratch. + /// + /// Returns the new bloom filter depth, that the traversal code is + /// responsible to keep around if it wants to get an effective filter. + pub fn insert_parents_recovering(&mut self, element: E, element_depth: usize) { + // Easy case, we're in a different restyle, or we're empty. + if self.elements.is_empty() { + self.rebuild(element); + return; + } + + let traversal_parent = match element.traversal_parent() { + Some(parent) => parent, + None => { + // Yay, another easy case. + self.clear(); + return; + }, + }; + + if self.current_parent() == Some(traversal_parent) { + // Ta da, cache hit, we're all done. + return; + } + + if element_depth == 0 { + self.clear(); + return; + } + + // We should've early exited above. + debug_assert!( + element_depth != 0, + "We should have already cleared the bloom filter" + ); + debug_assert!(!self.elements.is_empty(), "How! We should've just rebuilt!"); + + // Now the fun begins: We have the depth of the dom and the depth of the + // last element inserted in the filter, let's try to find a common + // parent. + // + // The current depth, that is, the depth of the last element inserted in + // the bloom filter, is the number of elements _minus one_, that is: if + // there's one element, it must be the root -> depth zero. + let mut current_depth = self.elements.len() - 1; + + // If the filter represents an element too deep in the dom, we need to + // pop ancestors. + while current_depth > element_depth - 1 { + self.pop().expect("Emilio is bad at math"); + current_depth -= 1; + } + + // Now let's try to find a common parent in the bloom filter chain, + // starting with traversal_parent. + let mut common_parent = traversal_parent; + let mut common_parent_depth = element_depth - 1; + + // Let's collect the parents we are going to need to insert once we've + // found the common one. + let mut parents_to_insert = SmallVec::<[E; 16]>::new(); + + // If the bloom filter still doesn't have enough elements, the common + // parent is up in the dom. + while common_parent_depth > current_depth { + // TODO(emilio): Seems like we could insert parents here, then + // reverse the slice. + parents_to_insert.push(common_parent); + common_parent = common_parent.traversal_parent().expect("We were lied to"); + common_parent_depth -= 1; + } + + // Now the two depths are the same. + debug_assert_eq!(common_parent_depth, current_depth); + + // Happy case: The parents match, we only need to push the ancestors + // we've collected and we'll never enter in this loop. + // + // Not-so-happy case: Parent's don't match, so we need to keep going up + // until we find a common ancestor. + // + // Gecko currently models native anonymous content that conceptually + // hangs off the document (such as scrollbars) as a separate subtree + // from the document root. + // + // Thus it's possible with Gecko that we do not find any common + // ancestor. + while *(self.elements.last().unwrap().element) != common_parent { + parents_to_insert.push(common_parent); + self.pop().unwrap(); + common_parent = match common_parent.traversal_parent() { + Some(parent) => parent, + None => { + debug_assert!(self.elements.is_empty()); + if cfg!(feature = "gecko") { + break; + } else { + panic!("should have found a common ancestor"); + } + }, + } + } + + // Now the parents match, so insert the stack of elements we have been + // collecting so far. + for parent in parents_to_insert.drain(..).rev() { + self.push(parent); + } + + debug_assert_eq!(self.elements.len(), element_depth); + + // We're done! Easy. + } +} |