orinium_browser/engine/layouter/
match_cache.rs1use std::collections::HashMap;
37use std::sync::Arc;
38
39use super::dom_snapshot::NodeId;
40use crate::engine::cache_stats::{CacheStats, CacheStatsSnapshot};
41use crate::engine::css::parser::ComplexSelector;
42
43pub const DEFAULT_CAPACITY: usize = 131_072;
47
48type MatchKey = (usize, NodeId);
50
51pub struct SelectorMatchCache {
62 entries: HashMap<MatchKey, (bool, u64)>,
63 epoch: u64,
64 capacity: usize,
65 stats: CacheStats,
66}
67
68impl Default for SelectorMatchCache {
69 fn default() -> Self {
70 Self::new()
71 }
72}
73
74impl SelectorMatchCache {
75 pub fn new() -> Self {
76 Self::with_capacity(DEFAULT_CAPACITY)
77 }
78
79 pub fn with_capacity(capacity: usize) -> Self {
80 let capacity = capacity.max(1);
81 Self {
82 entries: HashMap::with_capacity(capacity.min(4096)),
83 epoch: 0,
84 capacity,
85 stats: CacheStats::new("selector_match"),
86 }
87 }
88
89 pub fn get_or_insert(
95 &mut self,
96 selector: &Arc<ComplexSelector>,
97 node_id: NodeId,
98 compute: impl FnOnce() -> bool,
99 ) -> bool {
100 let key = (Arc::as_ptr(selector) as usize, node_id);
101 if let Some(&(matched, entry_epoch)) = self.entries.get(&key)
102 && entry_epoch == self.epoch
103 {
104 self.stats.record_hit();
105 return matched;
106 }
107 self.stats.record_miss();
110 let matched = compute();
111 if self.entries.len() < self.capacity {
112 self.entries.insert(key, (matched, self.epoch));
113 } else {
114 self.stats.record_skip();
115 }
116 matched
117 }
118
119 pub fn clear(&mut self) {
125 self.entries.clear();
126 self.epoch = self.epoch.wrapping_add(1);
127 self.stats.reset();
128 }
129
130 pub fn len(&self) -> usize {
131 self.entries.len()
132 }
133
134 pub fn is_empty(&self) -> bool {
135 self.entries.is_empty()
136 }
137
138 pub fn capacity(&self) -> usize {
139 self.capacity
140 }
141
142 pub fn stats(&self) -> CacheStatsSnapshot {
143 self.stats.snapshot()
144 }
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 fn selector() -> Arc<ComplexSelector> {
152 Arc::new(ComplexSelector { parts: Vec::new() })
153 }
154
155 #[test]
156 fn a_second_lookup_reuses_the_stored_result() {
157 let mut cache = SelectorMatchCache::new();
158 let selector = selector();
159 let mut computed = 0;
160
161 let first = cache.get_or_insert(&selector, 7, || {
162 computed += 1;
163 true
164 });
165 let second = cache.get_or_insert(&selector, 7, || {
166 computed += 1;
167 false
168 });
169
170 assert!(first);
171 assert!(second, "the cached result must win over the recomputation");
172 assert_eq!(computed, 1);
173 let stats = cache.stats();
174 assert_eq!(
175 (stats.hits, stats.misses, stats.evictions, stats.skips),
176 (1, 1, 0, 0)
177 );
178 }
179
180 #[test]
181 fn elements_and_selectors_are_distinct_keys() {
182 let mut cache = SelectorMatchCache::new();
183 let first_selector = selector();
184 let second_selector = selector();
185 let mut computed = 0;
186
187 cache.get_or_insert(&first_selector, 1, || {
188 computed += 1;
189 true
190 });
191 cache.get_or_insert(&first_selector, 2, || {
192 computed += 1;
193 false
194 });
195 cache.get_or_insert(&second_selector, 1, || {
196 computed += 1;
197 false
198 });
199
200 assert_eq!(computed, 3);
201 let stats = cache.stats();
202 assert_eq!(
203 (stats.hits, stats.misses, stats.evictions, stats.skips),
204 (0, 3, 0, 0)
205 );
206 }
207
208 #[test]
209 fn capacity_pressure_skips_without_eviction() {
210 let mut cache = SelectorMatchCache::with_capacity(1);
211 let first_selector = selector();
212 let second_selector = selector();
213
214 cache.get_or_insert(&first_selector, 1, || true);
215 cache.get_or_insert(&second_selector, 1, || true);
216
217 assert_eq!(cache.len(), 1);
218 let stats = cache.stats();
219 assert_eq!(stats.evictions, 0);
220 assert_eq!(stats.skips, 1);
221 assert_eq!(stats.hits, 0);
222
223 let mut computed = 0;
225 let matched = cache.get_or_insert(&first_selector, 1, || {
226 computed += 1;
227 false
228 });
229 assert!(matched);
230 assert_eq!(computed, 0);
231 assert_eq!(cache.stats().hits, 1);
232 }
233
234 #[test]
235 fn over_capacity_passes_hit_in_the_second_epoch_like_pass() {
236 let mut cache = SelectorMatchCache::with_capacity(4);
237 let selectors: Vec<Arc<ComplexSelector>> = (0..6)
238 .map(|_| Arc::new(ComplexSelector { parts: Vec::new() }))
239 .collect();
240 let mut computed = 0;
241
242 for sel in &selectors {
244 cache.get_or_insert(sel, 1, || {
245 computed += 1;
246 true
247 });
248 }
249 assert_eq!(cache.len(), 4);
250 let first = cache.stats();
251 assert_eq!(first.misses, 6);
252 assert_eq!(first.skips, 2);
253 assert_eq!(first.evictions, 0);
254 assert_eq!(computed, 6);
255
256 for sel in &selectors {
259 cache.get_or_insert(sel, 1, || {
260 computed += 1;
261 false
262 });
263 }
264 let second = cache.stats();
265 assert_eq!(second.misses, 8);
266 assert_eq!(second.hits, 4);
267 assert_eq!(second.skips, 4);
268 assert_eq!(computed, 8);
269 }
270
271 #[test]
272 fn clear_drops_entries_and_resets_counters() {
273 let mut cache = SelectorMatchCache::new();
274 let selector = selector();
275 cache.get_or_insert(&selector, 1, || true);
276 cache.clear();
277
278 assert!(cache.is_empty());
279 let stats = cache.stats();
280 assert_eq!(stats.misses, 0);
281 assert_eq!(stats.hits, 0);
282 assert_eq!(stats.evictions, 0);
283 assert_eq!(stats.skips, 0);
284 }
285
286 #[test]
287 fn clear_bumps_epoch_so_stale_entries_are_not_reused() {
288 let mut cache = SelectorMatchCache::new();
289 let selector = selector();
290
291 let first = cache.get_or_insert(&selector, 1, || true);
293 assert!(first);
294
295 cache.epoch = cache.epoch.wrapping_add(1);
297
298 let second = cache.get_or_insert(&selector, 1, || false);
300 assert!(!second, "stale epoch entry must not be reused");
301
302 let stats = cache.stats();
304 assert_eq!(stats.misses, 2);
305 assert_eq!(stats.hits, 0);
306 }
307
308 #[test]
309 fn epoch_rollover_does_not_break_cache() {
310 let mut cache = SelectorMatchCache::new();
311 let selector = selector();
312
313 for _ in 0..10_000 {
315 cache.epoch = cache.epoch.wrapping_add(1);
316 cache.get_or_insert(&selector, 1, || true);
317 cache.clear();
318 }
319
320 assert!(cache.is_empty());
321 }
322}