Skip to main content

style/
bloom.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5//! The style bloom filter is used as an optimization when matching deep
6//! descendant selectors.
7
8#![deny(missing_docs)]
9
10use crate::LocalName;
11use crate::dom::{SendElement, TElement};
12use atomic_refcell::{AtomicRefCell, AtomicRefMut};
13use selectors::bloom::BloomFilter;
14use smallvec::SmallVec;
15
16thread_local! {
17    /// Bloom filters are large allocations, so we store them in thread-local storage
18    /// such that they can be reused across style traversals. StyleBloom is responsible
19    /// for ensuring that the bloom filter is zeroed when it is dropped.
20    ///
21    /// We intentionally leak this from TLS because we don't have the guarantee
22    /// of TLS destructors to run in worker threads.
23    ///
24    /// Also, leaking it guarantees that we can borrow it indefinitely.
25    ///
26    /// We could change this once https://github.com/rayon-rs/rayon/issues/688
27    /// is fixed, hopefully, which point we'd need to change the filter member below to be an
28    /// arc and carry an owning reference around or so.
29    static BLOOM_KEY: &'static AtomicRefCell<BloomFilter> = Box::leak(Default::default());
30}
31
32/// A struct that allows us to fast-reject deep descendant selectors avoiding
33/// selector-matching.
34///
35/// This is implemented using a counting bloom filter, and it's a standard
36/// optimization. See Gecko's `AncestorFilter`, and Blink's and WebKit's
37/// `SelectorFilter`.
38///
39/// The constraints for Servo's style system are a bit different compared to
40/// traditional style systems given Servo does a parallel breadth-first
41/// traversal instead of a sequential depth-first traversal.
42///
43/// This implies that we need to track a bit more state than other browsers to
44/// ensure we're doing the correct thing during the traversal, and being able to
45/// apply this optimization effectively.
46///
47/// Concretely, we have a bloom filter instance per worker thread, and we track
48/// the current DOM depth in order to find a common ancestor when it doesn't
49/// match the previous element we've styled.
50///
51/// This is usually a pretty fast operation (we use to be one level deeper than
52/// the previous one), but in the case of work-stealing, we may needed to push
53/// and pop multiple elements.
54///
55/// See the `insert_parents_recovering`, where most of the magic happens.
56///
57/// Regarding thread-safety, this struct is safe because:
58///
59///  * We clear this after a restyle.
60///  * The DOM shape and attributes (and every other thing we access here) are
61///    immutable during a restyle.
62///
63pub struct StyleBloom<E: TElement> {
64    /// A handle to the bloom filter from the thread upon which this StyleBloom
65    /// was created. We use AtomicRefCell so that this is all |Send|, which allows
66    /// StyleBloom to live in ThreadLocalStyleContext, which is dropped from the
67    /// parent thread.
68    filter: AtomicRefMut<'static, BloomFilter>,
69
70    /// The stack of elements that this bloom filter contains, along with the
71    /// number of hashes pushed for each element.
72    elements: SmallVec<[PushedElement<E>; 16]>,
73
74    /// Stack of hashes that have been pushed onto this filter.
75    pushed_hashes: SmallVec<[u32; 64]>,
76}
77
78/// The very rough benchmarks in the selectors crate show clear()
79/// costing about 25 times more than remove_hash(). We use this to implement
80/// clear() more efficiently when only a small number of hashes have been
81/// pushed.
82///
83/// One subtly to note is that remove_hash() will not touch the value
84/// if the filter overflowed. However, overflow can only occur if we
85/// get 255 collisions on the same hash value, and 25 < 255.
86const MEMSET_CLEAR_THRESHOLD: usize = 25;
87
88struct PushedElement<E: TElement> {
89    /// The element that was pushed.
90    element: SendElement<E>,
91
92    /// The number of hashes pushed for the element.
93    num_hashes: usize,
94}
95
96impl<E: TElement> PushedElement<E> {
97    fn new(el: E, num_hashes: usize) -> Self {
98        PushedElement {
99            element: unsafe { SendElement::new(el) },
100            num_hashes,
101        }
102    }
103}
104
105/// Returns whether the attribute name is excluded from the bloom filter.
106///
107/// We do this for attributes that are very common but not commonly used in
108/// selectors.
109#[inline]
110pub fn is_attr_name_excluded_from_filter(name: &LocalName) -> bool {
111    *name == local_name!("class") || *name == local_name!("id") || *name == local_name!("style")
112}
113
114/// Gather all relevant hash for fast-reject filters from an element.
115pub fn each_relevant_element_hash<E, F>(element: E, mut f: F)
116where
117    E: TElement,
118    F: FnMut(u32),
119{
120    f(element.local_name().get_hash32());
121    f(element.namespace().get_hash32());
122
123    if let Some(id) = element.id() {
124        f(id.get_hash32());
125    }
126
127    element.each_class(|class| f(class.get_hash32()));
128
129    element.each_attr_name(|name| {
130        if !is_attr_name_excluded_from_filter(name) {
131            f(name.get_hash32())
132        }
133    });
134}
135
136impl<E: TElement> Drop for StyleBloom<E> {
137    fn drop(&mut self) {
138        // Leave the reusable bloom filter in a zeroed state.
139        self.clear();
140    }
141}
142
143impl<E: TElement> Default for StyleBloom<E> {
144    fn default() -> Self {
145        Self::new()
146    }
147}
148
149impl<E: TElement> StyleBloom<E> {
150    /// Create an empty `StyleBloom`. Because StyleBloom acquires the thread-
151    /// local filter buffer, creating multiple live StyleBloom instances at
152    /// the same time on the same thread will panic.
153
154    // Forced out of line to limit stack frame sizes after extra inlining from
155    // https://github.com/rust-lang/rust/pull/43931
156    //
157    // See https://github.com/servo/servo/pull/18420#issuecomment-328769322
158    #[inline(never)]
159    pub fn new() -> Self {
160        let filter = BLOOM_KEY.with(|b| b.borrow_mut());
161        debug_assert!(
162            filter.is_zeroed(),
163            "Forgot to zero the bloom filter last time"
164        );
165        StyleBloom {
166            filter,
167            elements: Default::default(),
168            pushed_hashes: Default::default(),
169        }
170    }
171
172    /// Return the bloom filter used properly by the `selectors` crate.
173    pub fn filter(&self) -> &BloomFilter {
174        &self.filter
175    }
176
177    /// Push an element to the bloom filter, knowing that it's a child of the
178    /// last element parent.
179    pub fn push(&mut self, element: E) {
180        if cfg!(debug_assertions) && self.elements.is_empty() {
181            assert!(element.traversal_parent().is_none());
182        }
183        self.push_internal(element);
184    }
185
186    /// Same as `push`, but without asserting, in order to use it from
187    /// `rebuild`.
188    fn push_internal(&mut self, element: E) {
189        let mut count = 0;
190        each_relevant_element_hash(element, |hash| {
191            count += 1;
192            self.filter.insert_hash(hash);
193            self.pushed_hashes.push(hash);
194        });
195        self.elements.push(PushedElement::new(element, count));
196    }
197
198    /// Pop the last element in the bloom filter and return it.
199    #[inline]
200    fn pop(&mut self) -> Option<E> {
201        let PushedElement {
202            element,
203            num_hashes,
204        } = self.elements.pop()?;
205        let popped_element = *element;
206
207        // Verify that the pushed hashes match the ones we'd get from the element.
208        let mut expected_hashes = vec![];
209        if cfg!(debug_assertions) {
210            each_relevant_element_hash(popped_element, |hash| expected_hashes.push(hash));
211        }
212
213        for _ in 0..num_hashes {
214            let hash = self.pushed_hashes.pop().unwrap();
215            debug_assert_eq!(expected_hashes.pop().unwrap(), hash);
216            self.filter.remove_hash(hash);
217        }
218
219        Some(popped_element)
220    }
221
222    /// Returns the DOM depth of elements that can be correctly
223    /// matched against the bloom filter (that is, the number of
224    /// elements in our list).
225    pub fn matching_depth(&self) -> usize {
226        self.elements.len()
227    }
228
229    /// Clears the bloom filter.
230    pub fn clear(&mut self) {
231        self.elements.clear();
232
233        if self.pushed_hashes.len() > MEMSET_CLEAR_THRESHOLD {
234            self.filter.clear();
235            self.pushed_hashes.clear();
236        } else {
237            for hash in self.pushed_hashes.drain(..) {
238                self.filter.remove_hash(hash);
239            }
240            debug_assert!(self.filter.is_zeroed());
241        }
242    }
243
244    /// Rebuilds the bloom filter up to the parent of the given element.
245    pub fn rebuild(&mut self, mut element: E) {
246        self.clear();
247
248        let mut parents_to_insert = SmallVec::<[E; 16]>::new();
249        while let Some(parent) = element.traversal_parent() {
250            parents_to_insert.push(parent);
251            element = parent;
252        }
253
254        for parent in parents_to_insert.drain(..).rev() {
255            self.push(parent);
256        }
257    }
258
259    /// In debug builds, asserts that all the parents of `element` are in the
260    /// bloom filter.
261    ///
262    /// Goes away in release builds.
263    pub fn assert_complete(&self, mut element: E) {
264        if cfg!(debug_assertions) {
265            let mut checked = 0;
266            while let Some(parent) = element.traversal_parent() {
267                assert_eq!(
268                    parent,
269                    *(self.elements[self.elements.len() - 1 - checked].element)
270                );
271                element = parent;
272                checked += 1;
273            }
274            assert_eq!(checked, self.elements.len());
275        }
276    }
277
278    /// Get the element that represents the chain of things inserted
279    /// into the filter right now.  That chain is the given element
280    /// (if any) and its ancestors.
281    #[inline]
282    pub fn current_parent(&self) -> Option<E> {
283        self.elements.last().map(|el| *el.element)
284    }
285
286    /// Insert the parents of an element in the bloom filter, trying to recover
287    /// the filter if the last element inserted doesn't match.
288    ///
289    /// Gets the element depth in the dom, to make it efficient, or if not
290    /// provided always rebuilds the filter from scratch.
291    ///
292    /// Returns the new bloom filter depth, that the traversal code is
293    /// responsible to keep around if it wants to get an effective filter.
294    pub fn insert_parents_recovering(&mut self, element: E, element_depth: usize) {
295        // Easy case, we're in a different restyle, or we're empty.
296        if self.elements.is_empty() {
297            self.rebuild(element);
298            return;
299        }
300
301        let traversal_parent = match element.traversal_parent() {
302            Some(parent) => parent,
303            None => {
304                // Yay, another easy case.
305                self.clear();
306                return;
307            },
308        };
309
310        if self.current_parent() == Some(traversal_parent) {
311            // Ta da, cache hit, we're all done.
312            return;
313        }
314
315        if element_depth == 0 {
316            self.clear();
317            return;
318        }
319
320        // We should've early exited above.
321        debug_assert!(
322            element_depth != 0,
323            "We should have already cleared the bloom filter"
324        );
325        debug_assert!(!self.elements.is_empty(), "How! We should've just rebuilt!");
326
327        // Now the fun begins: We have the depth of the dom and the depth of the
328        // last element inserted in the filter, let's try to find a common
329        // parent.
330        //
331        // The current depth, that is, the depth of the last element inserted in
332        // the bloom filter, is the number of elements _minus one_, that is: if
333        // there's one element, it must be the root -> depth zero.
334        let mut current_depth = self.elements.len() - 1;
335
336        // If the filter represents an element too deep in the dom, we need to
337        // pop ancestors.
338        while current_depth > element_depth - 1 {
339            self.pop().expect("Emilio is bad at math");
340            current_depth -= 1;
341        }
342
343        // Now let's try to find a common parent in the bloom filter chain,
344        // starting with traversal_parent.
345        let mut common_parent = traversal_parent;
346        let mut common_parent_depth = element_depth - 1;
347
348        // Let's collect the parents we are going to need to insert once we've
349        // found the common one.
350        let mut parents_to_insert = SmallVec::<[E; 16]>::new();
351
352        // If the bloom filter still doesn't have enough elements, the common
353        // parent is up in the dom.
354        while common_parent_depth > current_depth {
355            // TODO(emilio): Seems like we could insert parents here, then
356            // reverse the slice.
357            parents_to_insert.push(common_parent);
358            common_parent = common_parent.traversal_parent().expect("We were lied to");
359            common_parent_depth -= 1;
360        }
361
362        // Now the two depths are the same.
363        debug_assert_eq!(common_parent_depth, current_depth);
364
365        // Happy case: The parents match, we only need to push the ancestors
366        // we've collected and we'll never enter in this loop.
367        //
368        // Not-so-happy case: Parent's don't match, so we need to keep going up
369        // until we find a common ancestor.
370        //
371        // Gecko currently models native anonymous content that conceptually
372        // hangs off the document (such as scrollbars) as a separate subtree
373        // from the document root.
374        //
375        // Thus it's possible with Gecko that we do not find any common
376        // ancestor.
377        while *(self.elements.last().unwrap().element) != common_parent {
378            parents_to_insert.push(common_parent);
379            self.pop().unwrap();
380            common_parent = match common_parent.traversal_parent() {
381                Some(parent) => parent,
382                None => {
383                    debug_assert!(self.elements.is_empty());
384                    if cfg!(feature = "gecko") {
385                        break;
386                    } else {
387                        panic!("should have found a common ancestor");
388                    }
389                },
390            }
391        }
392
393        // Now the parents match, so insert the stack of elements we have been
394        // collecting so far.
395        for parent in parents_to_insert.drain(..).rev() {
396            self.push(parent);
397        }
398
399        debug_assert_eq!(self.elements.len(), element_depth);
400
401        // We're done! Easy.
402    }
403}
404
405pub(crate) trait AtomExt {
406    fn get_hash32(&self) -> u32;
407}
408
409impl<Static: string_cache::StaticAtomSet> AtomExt for string_cache::Atom<Static> {
410    fn get_hash32(&self) -> u32 {
411        let hash64 = self.get_hash();
412        (hash64 >> 32) as u32 ^ (hash64 as u32)
413    }
414}