Skip to main content

style/
traversal.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//! Traversing the DOM tree; the bloom filter.
6
7use crate::context::{ElementCascadeInputs, SharedStyleContext, StyleContext};
8use crate::data::{ElementData, ElementStyles, RestyleKind};
9use crate::dom::{OpaqueNode, TElement, TNode};
10use crate::invalidation::element::restyle_hints::RestyleHint;
11use crate::matching::MatchMethods;
12use crate::selector_parser::PseudoElement;
13use crate::sharing::StyleSharingTarget;
14use crate::style_resolver::{PseudoElementResolution, StyleResolverForElement};
15use crate::stylist::RuleInclusion;
16use crate::traversal_flags::TraversalFlags;
17use selectors::matching::SelectorCaches;
18#[cfg(feature = "gecko")]
19use selectors::parser::PseudoElement as PseudoElementTrait;
20use smallvec::SmallVec;
21use std::collections::HashMap;
22
23/// A cache from element reference to known-valid computed style.
24pub type UndisplayedStyleCache =
25    HashMap<selectors::OpaqueElement, servo_arc::Arc<crate::properties::ComputedValues>>;
26
27/// We use this structure, rather than just returning a boolean from pre_traverse,
28/// to enforce that callers process root invalidations before starting the traversal.
29pub struct PreTraverseToken<E: TElement>(Option<E>);
30impl<E: TElement> PreTraverseToken<E> {
31    /// Whether we should traverse children.
32    pub fn should_traverse(&self) -> bool {
33        self.0.is_some()
34    }
35
36    /// Returns the traversal root for the current traversal.
37    pub(crate) fn traversal_root(self) -> Option<E> {
38        self.0
39    }
40}
41
42/// A DOM Traversal trait, that is used to generically implement styling for
43/// Gecko and Servo.
44pub trait DomTraversal<E: TElement>: Sync {
45    /// Process `node` on the way down, before its children have been processed.
46    ///
47    /// The callback is invoked for each child node that should be processed by
48    /// the traversal.
49    fn process_preorder<F>(
50        &self,
51        context: &mut StyleContext<E>,
52        node: E::ConcreteNode,
53        note_child: F,
54    ) where
55        F: FnMut(E::ConcreteNode);
56
57    /// Process `node` on the way up, after its children have been processed.
58    ///
59    /// This is only executed if `needs_postorder_traversal` returns true.
60    fn process_postorder(&self, contect: &mut StyleContext<E>, node: E::ConcreteNode);
61
62    /// Boolean that specifies whether a bottom up traversal should be
63    /// performed.
64    ///
65    /// If it's false, then process_postorder has no effect at all.
66    fn needs_postorder_traversal() -> bool {
67        true
68    }
69
70    /// Handles the postorder step of the traversal, if it exists, by bubbling
71    /// up the parent chain.
72    ///
73    /// If we are the last child that finished processing, recursively process
74    /// our parent. Else, stop. Also, stop at the root.
75    ///
76    /// Thus, if we start with all the leaves of a tree, we end up traversing
77    /// the whole tree bottom-up because each parent will be processed exactly
78    /// once (by the last child that finishes processing).
79    ///
80    /// The only communication between siblings is that they both
81    /// fetch-and-subtract the parent's children count. This makes it safe to
82    /// call durign the parallel traversal.
83    fn handle_postorder_traversal(
84        &self,
85        context: &mut StyleContext<E>,
86        root: OpaqueNode,
87        mut node: E::ConcreteNode,
88        children_to_process: isize,
89    ) {
90        // If the postorder step is a no-op, don't bother.
91        if !Self::needs_postorder_traversal() {
92            return;
93        }
94
95        if children_to_process == 0 {
96            // We are a leaf. Walk up the chain.
97            loop {
98                self.process_postorder(context, node);
99                if node.opaque() == root {
100                    break;
101                }
102                let parent = node.traversal_parent().unwrap();
103                let remaining = parent.did_process_child();
104                if remaining != 0 {
105                    // The parent has other unprocessed descendants. We only
106                    // perform postorder processing after the last descendant
107                    // has been processed.
108                    break;
109                }
110
111                node = parent.as_node();
112            }
113        } else {
114            // Otherwise record the number of children to process when the time
115            // comes.
116            node.as_element()
117                .unwrap()
118                .store_children_to_process(children_to_process);
119        }
120    }
121
122    /// Style invalidations happen when traversing from a parent to its children.
123    /// However, this mechanism can't handle style invalidations on the root. As
124    /// such, we have a pre-traversal step to handle that part and determine whether
125    /// a full traversal is needed.
126    fn pre_traverse(root: E, shared_context: &SharedStyleContext) -> PreTraverseToken<E> {
127        use crate::invalidation::element::state_and_attributes::propagate_dirty_bit_up_to;
128
129        let traversal_flags = shared_context.traversal_flags;
130
131        let mut data = root.mutate_data();
132        let mut data = data.as_deref_mut();
133
134        if let Some(ref mut data) = data {
135            if !traversal_flags.for_animation_only() {
136                // Invalidate our style, and that of our siblings and
137                // descendants as needed.
138                let invalidation_result = data.invalidate_style_if_needed(
139                    root,
140                    shared_context,
141                    None,
142                    &mut SelectorCaches::default(),
143                );
144
145                if invalidation_result.has_invalidated_siblings() {
146                    let actual_root = root.as_node().parent_element_or_host().expect(
147                        "How in the world can you invalidate \
148                         siblings without a parent?",
149                    );
150                    propagate_dirty_bit_up_to(actual_root, root);
151                    return PreTraverseToken(Some(actual_root));
152                }
153            }
154        }
155
156        let should_traverse =
157            Self::element_needs_traversal(root, traversal_flags, data.as_mut().map(|d| &**d));
158
159        // If we're not going to traverse at all, we may need to clear some state
160        // off the root (which would normally be done at the end of recalc_style_at).
161        if !should_traverse && data.is_some() {
162            clear_state_after_traversing(root, data.unwrap(), traversal_flags);
163        }
164
165        PreTraverseToken(if should_traverse { Some(root) } else { None })
166    }
167
168    /// Returns true if traversal is needed for the given element and subtree.
169    fn element_needs_traversal(
170        el: E,
171        traversal_flags: TraversalFlags,
172        data: Option<&ElementData>,
173    ) -> bool {
174        debug!(
175            "element_needs_traversal({:?}, {:?}, {:?})",
176            el, traversal_flags, data
177        );
178
179        // Unwrap the data.
180        let data = match data {
181            Some(d) if d.has_styles() => d,
182            _ => return true,
183        };
184
185        if traversal_flags.for_animation_only() {
186            // In case of animation-only traversal we need to traverse the element if the element
187            // has animation only dirty descendants bit, or animation-only restyle hint.
188            return el.has_animation_only_dirty_descendants()
189                || data.hint.has_animation_hint_or_recascade();
190        }
191
192        // If the dirty descendants bit is set, we need to traverse no matter
193        // what. Skip examining the ElementData.
194        if el.has_dirty_descendants() {
195            return true;
196        }
197
198        // If we have a restyle hint or need to recascade, we need to visit the
199        // element.
200        //
201        // Note that this is different than checking has_current_styles_for_traversal(),
202        // since that can return true even if we have a restyle hint indicating
203        // that the element's descendants (but not necessarily the element) need
204        // restyling.
205        if !data.hint.is_empty() {
206            return true;
207        }
208
209        // Servo uses the post-order traversal for flow construction, so we need
210        // to traverse any element with damage so that we can perform fixup /
211        // reconstruction on our way back up the tree.
212        if cfg!(feature = "servo") && !data.damage.is_empty() {
213            return true;
214        }
215
216        trace!("{:?} doesn't need traversal", el);
217        false
218    }
219
220    /// Return the shared style context common to all worker threads.
221    fn shared_context(&self) -> &SharedStyleContext<'_>;
222}
223
224/// Manually resolve style by sequentially walking up the parent chain to the
225/// first styled Element, ignoring pending restyles. The resolved style is made
226/// available via a callback, and can be dropped by the time this function
227/// returns in the display:none subtree case.
228pub fn resolve_style<E>(
229    context: &mut StyleContext<E>,
230    element: E,
231    rule_inclusion: RuleInclusion,
232    pseudo: Option<&PseudoElement>,
233    mut undisplayed_style_cache: Option<&mut UndisplayedStyleCache>,
234) -> ElementStyles
235where
236    E: TElement,
237{
238    debug_assert!(
239        rule_inclusion == RuleInclusion::DefaultOnly
240            || pseudo.is_some_and(|p| p.is_before_or_after())
241            || element.borrow_data().is_none_or(|d| !d.has_styles()),
242        "Why are we here?"
243    );
244    debug_assert!(
245        rule_inclusion == RuleInclusion::All || undisplayed_style_cache.is_none(),
246        "can't use the cache for default styles only"
247    );
248
249    let mut ancestors_requiring_style_resolution = SmallVec::<[E; 16]>::new();
250
251    // Clear the bloom filter, just in case the caller is reusing TLS.
252    context.thread_local.bloom_filter.clear();
253
254    let mut style = None;
255    let mut ancestor = element.traversal_parent();
256    while let Some(current) = ancestor {
257        if rule_inclusion == RuleInclusion::All {
258            if let Some(data) = current.borrow_data() {
259                if let Some(ancestor_style) = data.styles.get_primary() {
260                    style = Some(ancestor_style.clone());
261                    break;
262                }
263            }
264        }
265        if let Some(ref mut cache) = undisplayed_style_cache {
266            if let Some(s) = cache.get(&current.opaque()) {
267                style = Some(s.clone());
268                break;
269            }
270        }
271        ancestors_requiring_style_resolution.push(current);
272        ancestor = current.traversal_parent();
273    }
274
275    if let Some(ancestor) = ancestor {
276        context.thread_local.bloom_filter.rebuild(ancestor);
277        context.thread_local.bloom_filter.push(ancestor);
278    }
279
280    let mut layout_parent_style = style.clone();
281    while let Some(style) = layout_parent_style.take() {
282        if !style.is_display_contents() {
283            layout_parent_style = Some(style);
284            break;
285        }
286
287        ancestor = ancestor.unwrap().traversal_parent();
288        layout_parent_style =
289            ancestor.and_then(|a| a.borrow_data().map(|data| data.styles.primary().clone()));
290    }
291
292    for ancestor in ancestors_requiring_style_resolution.iter().rev() {
293        context.thread_local.bloom_filter.assert_complete(*ancestor);
294        context.thread_local.current_dom_depth = context.thread_local.bloom_filter.matching_depth();
295
296        // Actually `PseudoElementResolution` doesn't really matter here.
297        // (but it does matter below!).
298        let primary_style = StyleResolverForElement::new(
299            *ancestor,
300            context,
301            rule_inclusion,
302            PseudoElementResolution::IfApplicable,
303        )
304        .resolve_primary_style(style.as_deref(), layout_parent_style.as_deref());
305
306        let is_display_contents = primary_style.style().is_display_contents();
307
308        style = Some(primary_style.style.0);
309        if !is_display_contents {
310            layout_parent_style = style.clone();
311        }
312
313        if let Some(ref mut cache) = undisplayed_style_cache {
314            cache.insert(ancestor.opaque(), style.clone().unwrap());
315        }
316        context.thread_local.bloom_filter.push(*ancestor);
317    }
318
319    context.thread_local.bloom_filter.assert_complete(element);
320    context.thread_local.current_dom_depth = context.thread_local.bloom_filter.matching_depth();
321    let styles: ElementStyles = StyleResolverForElement::new(
322        element,
323        context,
324        rule_inclusion,
325        PseudoElementResolution::Force,
326    )
327    .resolve_style(style.as_deref(), layout_parent_style.as_deref())
328    .into();
329
330    if let Some(ref mut cache) = undisplayed_style_cache {
331        cache.insert(element.opaque(), styles.primary().clone());
332    }
333
334    styles
335}
336
337/// Calculates the style for a single node.
338#[inline]
339#[allow(unsafe_code)]
340pub fn recalc_style_at<E, D, F>(
341    _traversal: &D,
342    context: &mut StyleContext<E>,
343    element: E,
344    data: &mut ElementData,
345    note_child: F,
346) where
347    E: TElement,
348    D: DomTraversal<E>,
349    F: FnMut(E::ConcreteNode),
350{
351    let flags = context.shared.traversal_flags;
352    let is_initial_style = !data.has_styles();
353
354    context.thread_local.statistics.elements_traversed += 1;
355    debug_assert!(
356        flags.intersects(TraversalFlags::AnimationOnly)
357            || is_initial_style
358            || !element.has_snapshot()
359            || element.handled_snapshot(),
360        "Should've handled snapshots here already"
361    );
362
363    let restyle_kind = data.restyle_kind(context.shared);
364    debug!(
365        "recalc_style_at: {:?} (restyle_kind={:?}, dirty_descendants={:?}, data={:?})",
366        element,
367        restyle_kind,
368        element.has_dirty_descendants(),
369        data
370    );
371
372    let mut child_restyle_hint = RestyleHint::empty();
373
374    // Compute style for this element if necessary.
375    if let Some(restyle_kind) = restyle_kind {
376        child_restyle_hint = compute_style(context, element, data, restyle_kind);
377
378        if !element.matches_user_and_content_rules() {
379            // We must always cascade native anonymous subtrees, since they
380            // may have pseudo-elements underneath that would inherit from the
381            // closest non-NAC ancestor instead of us.
382            child_restyle_hint |= RestyleHint::RECASCADE_SELF;
383        }
384
385        // If we're restyling this element to display:none, throw away all style
386        // data in the subtree, notify the caller to early-return.
387        if data.styles.is_display_none() {
388            debug!(
389                "{:?} style is display:none - clearing data from descendants.",
390                element
391            );
392            unsafe {
393                clear_descendant_data(element);
394            }
395        }
396
397        // Inform any paint worklets of changed style, to speculatively
398        // evaluate the worklet code. In the case that the size hasn't changed,
399        // this will result in increased concurrency between script and layout.
400        notify_paint_worklet(context, data);
401    } else {
402        debug_assert!(data.has_styles());
403        data.set_traversed_without_styling();
404    }
405
406    // Now that matching and cascading is done, clear the bits corresponding to
407    // those operations and compute the propagated restyle hint (unless we're
408    // not processing invalidations, in which case don't need to propagate it
409    // and must avoid clearing it).
410    debug_assert!(
411        flags.for_animation_only() || !data.hint.has_animation_hint(),
412        "animation restyle hint should be handled during \
413         animation-only restyles"
414    );
415    let mut propagated_hint = data.hint.propagate(&flags);
416    trace!(
417        "propagated_hint={:?}, restyle_requirement={:?}, \
418         is_display_none={:?}, implementing_pseudo={:?}",
419        propagated_hint,
420        child_restyle_hint,
421        data.styles.is_display_none(),
422        element.implemented_pseudo_element()
423    );
424
425    // Integrate the child cascade requirement into the propagated hint.
426    propagated_hint |= child_restyle_hint;
427
428    let has_dirty_descendants_for_this_restyle = if flags.for_animation_only() {
429        element.has_animation_only_dirty_descendants()
430    } else {
431        element.has_dirty_descendants()
432    };
433
434    // Before examining each child individually, try to prove that our children
435    // don't need style processing. They need processing if any of the following
436    // conditions hold:
437    //
438    //  * We have the dirty descendants bit.
439    //  * We're propagating a restyle hint.
440    //  * This is a servo non-incremental traversal.
441    //
442    // We only do this if we're not a display: none root, since in that case
443    // it's useless to style children.
444    let mut traverse_children =
445        has_dirty_descendants_for_this_restyle || !propagated_hint.is_empty();
446
447    traverse_children = traverse_children && !data.styles.is_display_none();
448
449    // Examine our children, and enqueue the appropriate ones for traversal.
450    if traverse_children {
451        note_children::<E, D, F>(
452            context,
453            element,
454            propagated_hint,
455            is_initial_style,
456            note_child,
457        );
458    }
459
460    // FIXME(bholley): Make these assertions pass for servo.
461    if cfg!(feature = "gecko") && cfg!(debug_assertions) && data.styles.is_display_none() {
462        debug_assert!(!element.has_dirty_descendants());
463        debug_assert!(!element.has_animation_only_dirty_descendants());
464    }
465
466    clear_state_after_traversing(element, data, flags);
467}
468
469fn clear_state_after_traversing<E>(element: E, data: &mut ElementData, flags: TraversalFlags)
470where
471    E: TElement,
472{
473    if flags.intersects(TraversalFlags::FinalAnimationTraversal) {
474        debug_assert!(flags.for_animation_only());
475        data.clear_restyle_flags_and_damage();
476        unsafe {
477            element.unset_animation_only_dirty_descendants();
478        }
479    }
480}
481
482fn compute_style<E>(
483    context: &mut StyleContext<E>,
484    element: E,
485    data: &mut ElementData,
486    kind: RestyleKind,
487) -> RestyleHint
488where
489    E: TElement,
490{
491    use crate::data::RestyleKind::*;
492
493    context.thread_local.statistics.elements_styled += 1;
494    debug!("compute_style: {:?} (kind={:?})", element, kind);
495
496    if data.has_styles() {
497        data.set_restyled();
498    }
499
500    let mut important_rules_changed = false;
501    let new_styles = match kind {
502        MatchAndCascade => {
503            debug_assert!(
504                !context.shared.traversal_flags.for_animation_only() || !data.has_styles(),
505                "MatchAndCascade shouldn't normally be processed during animation-only traversal"
506            );
507            // Ensure the bloom filter is up to date.
508            context
509                .thread_local
510                .bloom_filter
511                .insert_parents_recovering(element, context.thread_local.current_dom_depth);
512
513            context.thread_local.bloom_filter.assert_complete(element);
514            debug_assert_eq!(
515                context.thread_local.bloom_filter.matching_depth(),
516                context.thread_local.current_dom_depth
517            );
518
519            // This is only relevant for animations as of right now.
520            important_rules_changed = true;
521
522            let mut target = StyleSharingTarget::new(element);
523
524            // Now that our bloom filter is set up, try the style sharing
525            // cache.
526            match target.share_style_if_possible(context) {
527                Some(shared_styles) => {
528                    context.thread_local.statistics.styles_shared += 1;
529                    shared_styles
530                },
531                None => {
532                    context.thread_local.statistics.elements_matched += 1;
533                    // Perform the matching and cascading.
534                    let new_styles = {
535                        let mut resolver = StyleResolverForElement::new(
536                            element,
537                            context,
538                            RuleInclusion::All,
539                            PseudoElementResolution::IfApplicable,
540                        );
541
542                        resolver.resolve_style_with_default_parents()
543                    };
544
545                    let dom_depth = context.thread_local.current_dom_depth;
546                    context.thread_local.sharing_cache.insert_if_possible(
547                        &element,
548                        &new_styles.primary,
549                        Some(&mut target),
550                        dom_depth,
551                        context.shared,
552                    );
553
554                    new_styles
555                },
556            }
557        },
558        CascadeWithReplacements(flags) => {
559            // Skipping full matching, load cascade inputs from previous values.
560            let mut cascade_inputs = ElementCascadeInputs::new_from_element_data(data);
561            important_rules_changed = element.replace_rules(flags, context, &mut cascade_inputs);
562
563            let mut resolver = StyleResolverForElement::new(
564                element,
565                context,
566                RuleInclusion::All,
567                PseudoElementResolution::IfApplicable,
568            );
569
570            resolver.cascade_styles_with_default_parents(cascade_inputs)
571        },
572        CascadeOnly => {
573            // Skipping full matching, load cascade inputs from previous values.
574            let cascade_inputs = ElementCascadeInputs::new_from_element_data(data);
575
576            let new_styles = {
577                let mut resolver = StyleResolverForElement::new(
578                    element,
579                    context,
580                    RuleInclusion::All,
581                    PseudoElementResolution::IfApplicable,
582                );
583
584                resolver.cascade_styles_with_default_parents(cascade_inputs)
585            };
586
587            // Insert into the cache, but only if this style isn't reused from a
588            // sibling or cousin. Otherwise, recascading a bunch of identical
589            // elements would unnecessarily flood the cache with identical entries.
590            //
591            // This is analogous to the obvious "don't insert an element that just
592            // got a hit in the style sharing cache" behavior in the MatchAndCascade
593            // handling above.
594            //
595            // Note that, for the MatchAndCascade path, we still insert elements that
596            // shared styles via the rule node, because we know that there's something
597            // different about them that caused them to miss the sharing cache before
598            // selector matching. If we didn't, we would still end up with the same
599            // number of eventual styles, but would potentially miss out on various
600            // opportunities for skipping selector matching, which could hurt
601            // performance.
602            if !new_styles.primary.reused_via_rule_node {
603                context.thread_local.sharing_cache.insert_if_possible(
604                    &element,
605                    &new_styles.primary,
606                    None,
607                    context.thread_local.current_dom_depth,
608                    context.shared,
609                );
610            }
611
612            new_styles
613        },
614    };
615
616    element.finish_restyle(context, data, new_styles, important_rules_changed)
617}
618
619#[cfg(feature = "servo")]
620fn notify_paint_worklet<E>(context: &StyleContext<E>, data: &ElementData)
621where
622    E: TElement,
623{
624    use crate::values::generics::image::Image;
625    use style_traits::ToCss;
626
627    // We speculatively evaluate any paint worklets during styling.
628    // This allows us to run paint worklets in parallel with style and layout.
629    // Note that this is wasted effort if the size of the node has
630    // changed, but in may cases it won't have.
631    if let Some(ref values) = data.styles.primary {
632        for image in &values.get_background().background_image.0 {
633            let (name, arguments) = match *image {
634                Image::PaintWorklet(ref worklet) => (&worklet.name, &worklet.arguments),
635                _ => continue,
636            };
637            let painter = match context.shared.registered_speculative_painters.get(name) {
638                Some(painter) => painter,
639                None => continue,
640            };
641            let properties = painter
642                .properties()
643                .iter()
644                .filter_map(|(name, id)| id.as_shorthand().err().map(|id| (name, id)))
645                .map(|(name, id)| (name.clone(), values.computed_value_to_string(id)))
646                .collect();
647            let arguments = arguments
648                .iter()
649                .map(|argument| argument.to_css_string())
650                .collect();
651            debug!("Notifying paint worklet {}.", painter.name());
652            painter.speculatively_draw_a_paint_image(properties, arguments);
653        }
654    }
655}
656
657#[cfg(not(feature = "servo"))]
658fn notify_paint_worklet<E>(_context: &StyleContext<E>, _data: &ElementData)
659where
660    E: TElement,
661{
662    // The CSS paint API is Servo-only at the moment
663}
664
665fn note_children<E, D, F>(
666    context: &mut StyleContext<E>,
667    element: E,
668    propagated_hint: RestyleHint,
669    is_initial_style: bool,
670    mut note_child: F,
671) where
672    E: TElement,
673    D: DomTraversal<E>,
674    F: FnMut(E::ConcreteNode),
675{
676    trace!("note_children: {:?}", element);
677    let flags = context.shared.traversal_flags;
678
679    // Loop over all the traversal children.
680    for child_node in element.traversal_children() {
681        let Some(child) = child_node.as_element() else {
682            continue;
683        };
684
685        let mut child_data = child.mutate_data();
686        let mut child_data = child_data.as_deref_mut();
687        trace!(
688            " > {:?} -> {:?} + {:?}, pseudo: {:?}",
689            child,
690            child_data.as_ref().map(|d| d.hint),
691            propagated_hint,
692            child.implemented_pseudo_element()
693        );
694
695        if let Some(ref mut child_data) = child_data {
696            child_data.hint.insert(propagated_hint);
697
698            // Handle element snapshots and invalidation of descendants and siblings
699            // as needed.
700            //
701            // NB: This will be a no-op if there's no snapshot.
702            child_data.invalidate_style_if_needed(
703                child,
704                context.shared,
705                Some(&context.thread_local.stack_limit_checker),
706                &mut context.thread_local.selector_caches,
707            );
708        }
709
710        if D::element_needs_traversal(child, flags, child_data.map(|d| &*d)) {
711            note_child(child_node);
712
713            // Set the dirty descendants bit on the parent as needed, so that we
714            // can find elements during the post-traversal.
715            //
716            // Note that these bits may be cleared again at the bottom of
717            // recalc_style_at if requested by the caller.
718            if !is_initial_style {
719                if flags.for_animation_only() {
720                    unsafe {
721                        element.set_animation_only_dirty_descendants();
722                    }
723                } else {
724                    unsafe {
725                        element.set_dirty_descendants();
726                    }
727                }
728            }
729        }
730    }
731}
732
733/// Clear style data for all the subtree under `root` (but not for root itself).
734///
735/// We use a list to avoid unbounded recursion, which we need to avoid in the
736/// parallel traversal because the rayon stacks are small.
737pub unsafe fn clear_descendant_data<E>(root: E)
738where
739    E: TElement,
740{
741    let mut parents = SmallVec::<[E; 32]>::new();
742    parents.push(root);
743    while let Some(p) = parents.pop() {
744        for kid in p.traversal_children() {
745            if let Some(kid) = kid.as_element() {
746                // We maintain an invariant that, if an element has data, all its
747                // ancestors have data as well.
748                //
749                // By consequence, any element without data has no descendants with
750                // data.
751                if kid.has_data() {
752                    unsafe {
753                        kid.clear_data();
754                    }
755                    parents.push(kid);
756                }
757            }
758        }
759    }
760
761    // Make sure not to clear NODE_NEEDS_FRAME on the root.
762    unsafe {
763        root.clear_descendant_bits();
764    }
765}