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