repose_core/
scope_cache.rs1use rustc_hash::{FxHashMap, FxHashSet};
2use std::cell::RefCell;
3
4use crate::View;
5
6thread_local! {
7 static CURRENT_SCOPE_STACK: RefCell<Vec<String>> = const { RefCell::new(Vec::new()) };
13 static CURRENT_SCOPE_KEY: RefCell<Option<String>> =
15 const { RefCell::new(None) };
16
17 static SCOPE_SIGNAL_DEPS: RefCell<FxHashMap<usize, FxHashSet<String>>> =
21 RefCell::new(FxHashMap::default());
22
23 static SCOPE_TO_SIGNALS: RefCell<FxHashMap<String, FxHashSet<usize>>> =
26 RefCell::new(FxHashMap::default());
27}
28
29pub fn record_scope_signal_dep(signal_id: usize) {
34 let stack: Vec<String> = CURRENT_SCOPE_STACK.with(|s| s.borrow().clone());
35 let stack = if stack.is_empty() {
36 match CURRENT_SCOPE_KEY.with(|k| k.borrow().clone()) {
37 Some(k) => vec![k],
38 None => Vec::new(),
39 }
40 } else {
41 stack
42 };
43 if stack.is_empty() {
44 return;
45 }
46 SCOPE_SIGNAL_DEPS.with(|deps| {
47 let mut deps = deps.borrow_mut();
48 for key in &stack {
49 deps.entry(signal_id).or_default().insert(key.clone());
50 }
51 });
52 SCOPE_TO_SIGNALS.with(|m| {
53 let mut m = m.borrow_mut();
54 for key in &stack {
55 m.entry(key.clone()).or_default().insert(signal_id);
56 }
57 });
58}
59
60pub fn mark_scope_deps_dirty(signal_id: usize) {
63 let keys = SCOPE_SIGNAL_DEPS.with(|deps| deps.borrow().get(&signal_id).cloned());
64 if let Some(keys) = keys {
65 for key in keys {
66 crate::runtime::COMPOSER.with(|c| {
67 let mut c = c.borrow_mut();
68 if let Some(cache) = c.scope_caches.get_mut(&key) {
69 cache.clean = false;
70 }
71 });
72 }
73 }
74}
75
76pub fn with_scope_key<R>(key: &str, f: impl FnOnce() -> R) -> R {
79 struct Guard;
80 impl Drop for Guard {
81 fn drop(&mut self) {
82 if CURRENT_SCOPE_STACK
83 .try_with(|s| {
84 if let Ok(mut s) = s.try_borrow_mut() {
85 s.pop();
86 } else {
87 log::error!(
88 "scope_cache: scope stack busy during scope exit; scope entry leaked"
89 );
90 }
91 })
92 .is_err()
93 {
94 log::error!(
95 "scope_cache: scope stack unavailable during scope exit (thread teardown?)"
96 );
97 }
98 let top = CURRENT_SCOPE_STACK
99 .try_with(|s| s.try_borrow().ok().and_then(|s| s.last().cloned()))
100 .ok()
101 .flatten();
102 if CURRENT_SCOPE_KEY
103 .try_with(|k| {
104 if let Ok(mut k) = k.try_borrow_mut() {
105 *k = top;
106 } else {
107 log::error!(
108 "scope_cache: current scope key busy during scope exit; stale scope key retained"
109 );
110 }
111 })
112 .is_err()
113 {
114 log::error!(
115 "scope_cache: current scope key unavailable during scope exit (thread teardown?)"
116 );
117 }
118 }
119 }
120 CURRENT_SCOPE_STACK.with(|s| s.borrow_mut().push(key.to_string()));
121 CURRENT_SCOPE_KEY.with(|k| *k.borrow_mut() = Some(key.to_string()));
122 let _guard = Guard;
123 let result = f();
124 drop(_guard);
125 result
126}
127
128pub fn clear_scope_deps(key: &str) {
132 let signals = SCOPE_TO_SIGNALS.with(|m| m.borrow_mut().remove(key));
133 if let Some(signals) = signals {
134 SCOPE_SIGNAL_DEPS.with(|deps| {
135 let mut deps = deps.borrow_mut();
136 for signal_id in signals {
137 if let Some(scopes) = deps.get_mut(&signal_id) {
138 scopes.remove(key);
139 if scopes.is_empty() {
140 deps.remove(&signal_id);
141 }
142 }
143 }
144 });
145 }
146}
147
148pub struct ScopeCache {
150 pub input_hash: u64,
152 pub view: View,
154 pub slot_delta: usize,
156 pub clean: bool,
158}
159
160pub fn should_run(key: &str, input_hash: u64) -> bool {
162 crate::runtime::COMPOSER.with(|c| {
163 let c = c.borrow();
164 match c.scope_caches.get(key) {
165 Some(cache) => !cache.clean || cache.input_hash != input_hash,
166 None => true,
167 }
168 })
169}
170
171pub fn get_cached(key: &str, _s: &mut crate::runtime::Scheduler) -> View {
175 crate::runtime::COMPOSER.with(|c| {
176 let mut c = c.borrow_mut();
177 let (slot_delta, view) = {
178 let cache = c
179 .scope_caches
180 .get(key)
181 .expect("scope_cache::get_cached called but no cache entry found");
182 (cache.slot_delta, cache.view.clone())
183 };
184
185 c.cursor += slot_delta;
186 view
187 })
188}
189
190pub fn set_cache(key: &str, input_hash: u64, view: View, slot_delta: usize) {
192 crate::runtime::COMPOSER.with(|c| {
193 let mut c = c.borrow_mut();
194 c.scope_caches.insert(
195 key.to_string(),
196 ScopeCache {
197 input_hash,
198 view,
199 slot_delta,
200 clean: true,
201 },
202 );
203 });
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209 use crate::signal::signal;
210
211 fn reset_maps() {
212 SCOPE_SIGNAL_DEPS.with(|d| d.borrow_mut().clear());
213 SCOPE_TO_SIGNALS.with(|d| d.borrow_mut().clear());
214 }
215
216 #[test]
217 fn scope_deps_deduplicate_keys() {
218 reset_maps();
219 let sig = signal(0);
220
221 with_scope_key("dedupe_scope", || {
223 let _ = sig.get();
224 let _ = sig.get();
225 });
226
227 SCOPE_SIGNAL_DEPS.with(|d| {
228 let d = d.borrow();
229 assert_eq!(
230 d.get(&sig.id()).map(|s| s.len()),
231 Some(1),
232 "duplicate signal reads must collapse to a single scope dep"
233 );
234 });
235 SCOPE_TO_SIGNALS.with(|d| {
236 let d = d.borrow();
237 assert_eq!(d.get("dedupe_scope").map(|s| s.len()), Some(1));
238 });
239
240 clear_scope_deps("dedupe_scope");
242 SCOPE_TO_SIGNALS.with(|d| assert!(d.borrow().is_empty()));
243 SCOPE_SIGNAL_DEPS.with(|d| assert!(d.borrow().is_empty()));
244 }
245
246 #[test]
247 fn scope_deps_multiple_scopes_share_signal() {
248 reset_maps();
249 let sig = signal(0);
250
251 with_scope_key("scope_a", || {
252 let _ = sig.get();
253 });
254 with_scope_key("scope_b", || {
255 let _ = sig.get();
256 });
257
258 SCOPE_SIGNAL_DEPS.with(|d| {
259 let d = d.borrow();
260 let scopes = d.get(&sig.id()).unwrap();
261 assert!(scopes.contains("scope_a"));
262 assert!(scopes.contains("scope_b"));
263 });
264
265 clear_scope_deps("scope_a");
267 SCOPE_SIGNAL_DEPS.with(|d| {
268 let d = d.borrow();
269 let scopes = d.get(&sig.id()).unwrap();
270 assert!(!scopes.contains("scope_a"));
271 assert!(scopes.contains("scope_b"));
272 });
273 SCOPE_TO_SIGNALS.with(|d| {
274 assert!(d.borrow().get("scope_a").is_none());
275 assert!(d.borrow().get("scope_b").is_some());
276 });
277 }
278}