From 28bd450787b5c6d07c484d1646c8efb3ea08c587 Mon Sep 17 00:00:00 2001 From: Glint Dev Date: Fri, 24 Jul 2026 18:47:41 +0300 Subject: [PATCH] Phase 8: StyleCache memoization Add StyleCache struct with hash-keyed LRU-eviction strategy and integrate into StyleSheet via Mutex so no signature changes are needed on evaluate_vdom_incr. - StyleCache::get_or_compute hashes (type_name, props, epoch) -> ComputedStyle - LRU: full clear on overflow (max 1024) - RefCell avoided in favor of Mutex so StyleSheet remains Sync for parallel matching_rules_batch - Manual Clone impl skips cache (fresh cache on clone) - Call sites in evaluate_vdom_incr switched from ComputedStyle::compute to stylesheet.compute_cached - Cache cleared in build_index after epoch increment (stale entries) --- src/interpreter/mod.rs | 4 +- src/interpreter/style.rs | 80 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 2 deletions(-) diff --git a/src/interpreter/mod.rs b/src/interpreter/mod.rs index c432a89..66cbed3 100644 --- a/src/interpreter/mod.rs +++ b/src/interpreter/mod.rs @@ -469,7 +469,7 @@ impl Interpreter { } let matched_sheets = Self::collect_matching_styles(&vcomp, stylesheet, &[], &structural, ancestors, &sibling_infos[0..i]); - vcomp.computed_style = ComputedStyle::compute(&vcomp.properties, &matched_sheets); + vcomp.computed_style = stylesheet.compute_cached(vcomp.type_name, &vcomp.properties, &matched_sheets); let child_ancestors = Self::build_ancestor_chain(ancestors, &vcomp); let comp_child_refs: Vec<&Element<'a>> = comp.children.iter().collect(); @@ -502,7 +502,7 @@ impl Interpreter { } let matched_sheets = Self::collect_matching_styles(&vnode, stylesheet, &[], &structural, ancestors, &sibling_infos[0..i]); - vnode.computed_style = ComputedStyle::compute(&vnode.properties, &matched_sheets); + vnode.computed_style = stylesheet.compute_cached(vnode.type_name, &vnode.properties, &matched_sheets); let child_ancestors = Self::build_ancestor_chain(ancestors, &vnode); let child_refs: Vec<&Element<'a>> = el.children.iter().collect(); diff --git a/src/interpreter/style.rs b/src/interpreter/style.rs index 1580875..719010d 100644 --- a/src/interpreter/style.rs +++ b/src/interpreter/style.rs @@ -1,5 +1,9 @@ use std::collections::{HashMap, HashSet}; use std::borrow::Cow; +use std::sync::Mutex; +use std::hash::{Hash, Hasher}; + +use super::reactive::ElementId; #[derive(Debug, Clone)] @@ -458,10 +462,70 @@ impl StyleRule { #[derive(Debug, Clone)] +pub struct StyleCache { + entries: HashMap, + max_entries: usize, +} + +impl StyleCache { + pub fn new(max_entries: usize) -> Self { + Self { entries: HashMap::new(), max_entries } + } + + pub fn get_or_compute( + &mut self, + type_name: &str, + props: &[(Cow<'_, str>, Cow<'_, str>)], + epoch: u64, + matched_sheets: &[&HashMap], + ) -> ComputedStyle { + let key = { + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + type_name.hash(&mut hasher); + for (k, v) in props { + k.hash(&mut hasher); + v.hash(&mut hasher); + } + epoch.hash(&mut hasher); + hasher.finish() + }; + + if let Some(cached) = self.entries.get(&key) { + return cached.clone(); + } + + let style = ComputedStyle::compute(props, matched_sheets); + + if self.entries.len() >= self.max_entries { + self.entries.clear(); + } + self.entries.insert(key, style.clone()); + + style + } + + pub fn clear(&mut self) { + self.entries.clear(); + } +} + +#[derive(Debug)] pub struct StyleSheet { rules: Vec, index: Option, epoch: u64, + cache: Mutex, +} + +impl Clone for StyleSheet { + fn clone(&self) -> Self { + Self { + rules: self.rules.clone(), + index: self.index.clone(), + epoch: self.epoch, + cache: Mutex::new(StyleCache::new(1024)), + } + } } impl Default for StyleSheet { @@ -470,6 +534,7 @@ impl Default for StyleSheet { rules: Vec::new(), index: None, epoch: 0, + cache: Mutex::new(StyleCache::new(1024)), } } } @@ -488,6 +553,7 @@ impl StyleSheet { pub fn build_index(&mut self) { self.epoch += 1; + self.cache.lock().unwrap().clear(); let mut index = StyleIndex::new(); index.epoch = self.epoch; @@ -749,6 +815,20 @@ impl StyleSheet { props } + pub fn compute_cached<'a>( + &self, + type_name: &str, + props: &[(Cow<'_, str>, Cow<'_, str>)], + matched_sheets: &[&'a HashMap], + ) -> ComputedStyle { + let epoch = self.epoch; + self.cache.lock().unwrap().get_or_compute(type_name, props, epoch, matched_sheets) + } + + pub fn clear_cache(&self) { + self.cache.lock().unwrap().clear(); + } + pub fn is_empty(&self) -> bool { self.rules.is_empty() }