Skip to main content

style/
rule_cache.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//! A cache from rule node to computed values, in order to cache reset
6//! properties.
7
8use crate::computed_value_flags::ComputedValueFlags;
9use crate::context::CascadeInputs;
10use crate::logical_geometry::WritingMode;
11use crate::properties::{ComputedValues, StyleBuilder};
12use crate::rule_tree::{RuleCascadeFlags, StrongRuleNode};
13use crate::selector_parser::PseudoElement;
14use crate::shared_lock::StylesheetGuards;
15use crate::values::computed::{Context, NonNegativeLength};
16use crate::values::specified::color::ColorSchemeFlags;
17use rustc_hash::FxHashMap;
18use servo_arc::Arc;
19use smallvec::SmallVec;
20
21/// The conditions for caching and matching a style in the rule cache.
22#[derive(Clone, Debug, Default)]
23pub struct RuleCacheConditions {
24    uncacheable: bool,
25    font_size: Option<NonNegativeLength>,
26    line_height: Option<NonNegativeLength>,
27    writing_mode: Option<WritingMode>,
28    color_scheme: Option<ColorSchemeFlags>,
29}
30
31impl RuleCacheConditions {
32    /// Sets the style as depending in the font-size value.
33    pub fn set_font_size_dependency(&mut self, font_size: NonNegativeLength) {
34        debug_assert!(self.font_size.map_or(true, |f| f == font_size));
35        self.font_size = Some(font_size);
36    }
37
38    /// Sets the style as depending in the line-height value.
39    pub fn set_line_height_dependency(&mut self, line_height: NonNegativeLength) {
40        debug_assert!(self.line_height.map_or(true, |l| l == line_height));
41        self.line_height = Some(line_height);
42    }
43
44    /// Sets the style as depending in the color-scheme property value.
45    pub fn set_color_scheme_dependency(&mut self, color_scheme: ColorSchemeFlags) {
46        debug_assert!(self.color_scheme.map_or(true, |cs| cs == color_scheme));
47        self.color_scheme = Some(color_scheme);
48    }
49
50    /// Sets the style as uncacheable.
51    pub fn set_uncacheable(&mut self) {
52        self.uncacheable = true;
53    }
54
55    /// Sets the style as depending in the writing-mode value `writing_mode`.
56    pub fn set_writing_mode_dependency(&mut self, writing_mode: WritingMode) {
57        debug_assert!(self.writing_mode.map_or(true, |wm| wm == writing_mode));
58        self.writing_mode = Some(writing_mode);
59    }
60
61    /// Returns whether the current style's reset properties are cacheable.
62    fn cacheable(&self) -> bool {
63        !self.uncacheable
64    }
65}
66
67#[derive(Debug)]
68struct CachedConditions {
69    font_size: Option<NonNegativeLength>,
70    line_height: Option<NonNegativeLength>,
71    color_scheme: Option<ColorSchemeFlags>,
72    writing_mode: Option<WritingMode>,
73}
74
75impl CachedConditions {
76    /// Returns whether `style` matches the conditions.
77    fn matches(&self, cached_style: &ComputedValues, style: &StyleBuilder) -> bool {
78        if cached_style.effective_zoom != style.effective_zoom {
79            return false;
80        }
81
82        if cached_style
83            .flags
84            .intersects(ComputedValueFlags::IS_IN_APPEARANCE_BASE_SUBTREE)
85            != style
86                .flags()
87                .intersects(ComputedValueFlags::IS_IN_APPEARANCE_BASE_SUBTREE)
88        {
89            return false;
90        }
91
92        if let Some(fs) = self.font_size {
93            if style.get_font().clone_font_size().computed_size != fs {
94                return false;
95            }
96        }
97
98        if let Some(lh) = self.line_height {
99            let new_line_height =
100                style
101                    .device
102                    .calc_line_height(&style.get_font(), style.writing_mode, None);
103            if new_line_height != lh {
104                return false;
105            }
106        }
107
108        if self
109            .color_scheme
110            .is_some_and(|cs| style.get_inherited_ui().color_scheme_bits() != cs)
111        {
112            return false;
113        }
114
115        if self.writing_mode.is_some_and(|wm| style.writing_mode != wm) {
116            return false;
117        }
118
119        true
120    }
121}
122
123/// A TLS cache from rules matched to computed values.
124pub struct RuleCache {
125    // FIXME(emilio): Consider using LRUCache or something like that?
126    map: FxHashMap<StrongRuleNode, SmallVec<[(CachedConditions, Arc<ComputedValues>); 1]>>,
127}
128
129impl RuleCache {
130    /// Creates an empty `RuleCache`.
131    pub fn new() -> Self {
132        Self {
133            map: FxHashMap::default(),
134        }
135    }
136
137    /// Walk the rule tree and return a rule node for using as the key
138    /// for rule cache.
139    ///
140    /// It currently skips animation / style attribute / preshint rules when they don't contain any
141    /// declaration of a reset property. We don't skip other levels because walking the whole
142    /// parent chain can be expensive.
143    ///
144    /// TODO(emilio): Measure this, this was not super-well measured for performance (this was done
145    /// for memory in bug 1427681)... Walking the rule tree might be worth it if we hit the cache
146    /// enough?
147    fn get_rule_node_for_cache<'r>(
148        guards: &StylesheetGuards,
149        mut rule_node: Option<&'r StrongRuleNode>,
150    ) -> Option<&'r StrongRuleNode> {
151        use crate::rule_tree::CascadeOrigin;
152        while let Some(node) = rule_node {
153            let priority = node.cascade_priority();
154            let cascade_level = priority.cascade_level();
155            let should_try_to_skip = cascade_level.is_animation()
156                || cascade_level.origin() == CascadeOrigin::PresHints
157                || priority.layer_order().is_style_attribute_layer();
158            if !should_try_to_skip {
159                break;
160            }
161            if let Some(source) = node.style_source() {
162                let decls = source.get().read_with(cascade_level.guard(guards));
163                if decls.contains_any_reset() {
164                    break;
165                }
166            }
167            rule_node = node.parent();
168        }
169        rule_node
170    }
171
172    /// Finds a node in the properties matched cache.
173    ///
174    /// This needs to receive a `StyleBuilder` with the `early` properties
175    /// already applied.
176    pub fn find(&self, guards: &StylesheetGuards, context: &Context) -> Option<&ComputedValues> {
177        // A pseudo-element with property restrictions can result in different
178        // computed values if it's also used for a non-pseudo.
179        if context
180            .builder
181            .pseudo
182            .and_then(|p| p.property_restriction())
183            .is_some()
184        {
185            return None;
186        }
187
188        if context
189            .included_cascade_flags
190            .contains(RuleCascadeFlags::STARTING_STYLE)
191        {
192            // We don't want to cache nor include starting-style rules.
193            return None;
194        }
195
196        let rules = context.builder.rules.as_ref();
197        let rules = Self::get_rule_node_for_cache(guards, rules)?;
198        let cached_values = self.map.get(rules)?;
199
200        for &(ref conditions, ref values) in cached_values.iter() {
201            if conditions.matches(values, &context.builder) {
202                debug!("Using cached reset style with conditions {:?}", conditions);
203                return Some(&**values);
204            }
205        }
206        None
207    }
208
209    /// Inserts a node into the rules cache if possible.
210    ///
211    /// Returns whether the style was inserted into the cache.
212    pub fn insert_if_possible(
213        &mut self,
214        guards: &StylesheetGuards,
215        style: &Arc<ComputedValues>,
216        pseudo: Option<&PseudoElement>,
217        inputs: &CascadeInputs,
218        conditions: &RuleCacheConditions,
219    ) -> bool {
220        if !conditions.cacheable() {
221            return false;
222        }
223
224        // A pseudo-element with property restrictions can result in different computed values if
225        // it's also used for a non-pseudo.
226        // TODO: we could consider inserting them and just checking the builder like we do for zoom.
227        if pseudo.and_then(|p| p.property_restriction()).is_some() {
228            return false;
229        }
230
231        // Don't insert @starting-style styles in the cache, for the same reason.
232        if inputs
233            .included_cascade_flags
234            .contains(RuleCascadeFlags::STARTING_STYLE)
235        {
236            return false;
237        }
238
239        let rules = style.rules.as_ref();
240        let rules = match Self::get_rule_node_for_cache(guards, rules) {
241            Some(r) => r.clone(),
242            None => return false,
243        };
244
245        debug!(
246            "Inserting cached reset style with conditions {:?}",
247            conditions
248        );
249        let cached_conditions = CachedConditions {
250            writing_mode: conditions.writing_mode,
251            font_size: conditions.font_size,
252            line_height: conditions.line_height,
253            color_scheme: conditions.color_scheme,
254        };
255        self.map
256            .entry(rules)
257            .or_default()
258            .push((cached_conditions, style.clone()));
259        true
260    }
261}