windjammer_ui/
reactivity_optimized.rs

1//! Optimized reactivity system with performance improvements
2//!
3//! Key optimizations:
4//! 1. SmallVec for subscribers (avoid heap allocation for <= 4 subscribers)
5//! 2. Batched updates to avoid redundant notifications
6//! 3. Copy optimization for cheap-to-copy types
7//! 4. Effect deduplication
8
9use smallvec::SmallVec;
10use std::cell::RefCell;
11use std::collections::{HashMap, HashSet};
12use std::rc::Rc;
13use std::sync::atomic::{AtomicUsize, Ordering};
14
15pub type SignalId = usize;
16pub type EffectId = usize;
17
18thread_local! {
19    static REACTIVE_CONTEXT: RefCell<ReactiveContext> = RefCell::new(ReactiveContext::default());
20    static EFFECT_REGISTRY: RefCell<HashMap<EffectId, Effect>> = RefCell::new(HashMap::new());
21    static UPDATE_BATCH: RefCell<UpdateBatch> = RefCell::new(UpdateBatch::default());
22}
23
24#[derive(Default)]
25struct ReactiveContext {
26    current_effect: Option<EffectId>,
27}
28
29struct Effect {
30    f: Box<dyn Fn()>,
31}
32
33/// Batch multiple updates together to avoid redundant notifications
34#[derive(Default)]
35struct UpdateBatch {
36    pending_effects: HashSet<EffectId>,
37    is_batching: bool,
38}
39
40impl UpdateBatch {
41    fn start_batch() {
42        UPDATE_BATCH.with(|batch| {
43            batch.borrow_mut().is_batching = true;
44        });
45    }
46
47    fn end_batch() {
48        UPDATE_BATCH.with(|batch| {
49            let mut batch = batch.borrow_mut();
50            batch.is_batching = false;
51
52            // Execute all pending effects
53            let pending: Vec<_> = batch.pending_effects.drain().collect();
54            drop(batch); // Release borrow before executing effects
55
56            for effect_id in pending {
57                EFFECT_REGISTRY.with(|registry| {
58                    if let Some(effect) = registry.borrow().get(&effect_id) {
59                        (effect.f)();
60                    }
61                });
62            }
63        });
64    }
65
66    fn add_effect(effect_id: EffectId) {
67        UPDATE_BATCH.with(|batch| {
68            let mut batch = batch.borrow_mut();
69            if batch.is_batching {
70                batch.pending_effects.insert(effect_id);
71            } else {
72                // Execute immediately if not batching
73                drop(batch); // Release borrow
74                EFFECT_REGISTRY.with(|registry| {
75                    if let Some(effect) = registry.borrow().get(&effect_id) {
76                        (effect.f)();
77                    }
78                });
79            }
80        });
81    }
82}
83
84/// Optimized Signal using SmallVec for subscribers
85#[derive(Clone)]
86pub struct Signal<T: Clone> {
87    id: SignalId,
88    value: Rc<RefCell<T>>,
89    // Use SmallVec to avoid heap allocation for <= 4 subscribers
90    subscribers: Rc<RefCell<SmallVec<[EffectId; 4]>>>,
91}
92
93impl<T: Clone> Signal<T> {
94    pub fn new(value: T) -> Self {
95        static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
96        Self {
97            id: NEXT_ID.fetch_add(1, Ordering::Relaxed),
98            value: Rc::new(RefCell::new(value)),
99            subscribers: Rc::new(RefCell::new(SmallVec::new())),
100        }
101    }
102
103    pub fn get(&self) -> T {
104        // Track this read in the current reactive context
105        REACTIVE_CONTEXT.with(|ctx| {
106            let ctx = ctx.borrow();
107            if let Some(effect_id) = ctx.current_effect {
108                let mut subs = self.subscribers.borrow_mut();
109                if !subs.contains(&effect_id) {
110                    subs.push(effect_id);
111                }
112            }
113        });
114
115        self.value.borrow().clone()
116    }
117
118    pub fn get_untracked(&self) -> T {
119        self.value.borrow().clone()
120    }
121
122    pub fn set(&self, value: T) {
123        *self.value.borrow_mut() = value;
124        self.notify();
125    }
126
127    pub fn update<F>(&self, f: F)
128    where
129        F: FnOnce(&mut T),
130    {
131        f(&mut self.value.borrow_mut());
132        self.notify();
133    }
134
135    fn notify(&self) {
136        let subscribers = self.subscribers.borrow().clone();
137        for effect_id in subscribers.iter() {
138            UpdateBatch::add_effect(*effect_id);
139        }
140    }
141
142    pub fn id(&self) -> SignalId {
143        self.id
144    }
145}
146
147/// Optimized Signal for Copy types (no cloning needed)
148#[derive(Clone)]
149pub struct CopySignal<T: Copy> {
150    id: SignalId,
151    value: Rc<RefCell<T>>,
152    subscribers: Rc<RefCell<SmallVec<[EffectId; 4]>>>,
153}
154
155impl<T: Copy> CopySignal<T> {
156    pub fn new(value: T) -> Self {
157        static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
158        Self {
159            id: NEXT_ID.fetch_add(1, Ordering::Relaxed),
160            value: Rc::new(RefCell::new(value)),
161            subscribers: Rc::new(RefCell::new(SmallVec::new())),
162        }
163    }
164
165    pub fn get(&self) -> T {
166        REACTIVE_CONTEXT.with(|ctx| {
167            let ctx = ctx.borrow();
168            if let Some(effect_id) = ctx.current_effect {
169                let mut subs = self.subscribers.borrow_mut();
170                if !subs.contains(&effect_id) {
171                    subs.push(effect_id);
172                }
173            }
174        });
175
176        *self.value.borrow()
177    }
178
179    pub fn get_untracked(&self) -> T {
180        *self.value.borrow()
181    }
182
183    pub fn set(&self, value: T) {
184        *self.value.borrow_mut() = value;
185        self.notify();
186    }
187
188    pub fn update<F>(&self, f: F)
189    where
190        F: FnOnce(&mut T),
191    {
192        f(&mut self.value.borrow_mut());
193        self.notify();
194    }
195
196    fn notify(&self) {
197        let subscribers = self.subscribers.borrow().clone();
198        for effect_id in subscribers.iter() {
199            UpdateBatch::add_effect(*effect_id);
200        }
201    }
202
203    pub fn id(&self) -> SignalId {
204        self.id
205    }
206}
207
208/// Create an effect that runs when its dependencies change
209pub fn create_effect<F>(f: F) -> EffectId
210where
211    F: Fn() + 'static,
212{
213    static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
214    let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
215
216    let effect = Effect { f: Box::new(f) };
217    EFFECT_REGISTRY.with(|registry| {
218        registry.borrow_mut().insert(id, effect);
219    });
220
221    // Run the effect once to establish dependencies
222    REACTIVE_CONTEXT.with(|ctx| {
223        ctx.borrow_mut().current_effect = Some(id);
224    });
225
226    EFFECT_REGISTRY.with(|registry| {
227        if let Some(effect) = registry.borrow().get(&id) {
228            (effect.f)();
229        }
230    });
231
232    REACTIVE_CONTEXT.with(|ctx| {
233        ctx.borrow_mut().current_effect = None;
234    });
235
236    id
237}
238
239/// Batch multiple updates together
240pub fn batch<F, R>(f: F) -> R
241where
242    F: FnOnce() -> R,
243{
244    UpdateBatch::start_batch();
245    let result = f();
246    UpdateBatch::end_batch();
247    result
248}
249
250/// Dispose an effect
251pub fn dispose_effect(id: EffectId) {
252    EFFECT_REGISTRY.with(|registry| {
253        registry.borrow_mut().remove(&id);
254    });
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260
261    #[test]
262    fn test_signal_basic() {
263        let signal = Signal::new(42);
264        assert_eq!(signal.get(), 42);
265
266        signal.set(100);
267        assert_eq!(signal.get(), 100);
268    }
269
270    #[test]
271    fn test_copy_signal() {
272        let signal = CopySignal::new(42);
273        assert_eq!(signal.get(), 42);
274
275        signal.set(100);
276        assert_eq!(signal.get(), 100);
277    }
278
279    #[test]
280    fn test_effect() {
281        let signal = Signal::new(0);
282        let result = Rc::new(RefCell::new(0));
283        let result_clone = result.clone();
284        let signal_clone = signal.clone();
285
286        let _effect_id = create_effect(move || {
287            *result_clone.borrow_mut() = signal_clone.get();
288        });
289
290        assert_eq!(*result.borrow(), 0);
291
292        signal.set(42);
293        assert_eq!(*result.borrow(), 42);
294    }
295
296    #[test]
297    fn test_batched_updates() {
298        let signal1 = Signal::new(0);
299        let signal2 = Signal::new(0);
300        let count = Rc::new(RefCell::new(0));
301        let count_clone = count.clone();
302        let signal1_clone = signal1.clone();
303        let signal2_clone = signal2.clone();
304
305        let _effect_id = create_effect(move || {
306            let _ = signal1_clone.get();
307            let _ = signal2_clone.get();
308            *count_clone.borrow_mut() += 1;
309        });
310
311        // Reset count after initial effect run
312        *count.borrow_mut() = 0;
313
314        // Without batching, this would trigger 2 effect runs
315        batch(|| {
316            signal1.set(1);
317            signal2.set(2);
318        });
319
320        // With batching, effect should only run once
321        assert_eq!(*count.borrow(), 1);
322    }
323
324    #[test]
325    fn test_smallvec_optimization() {
326        let signal = Signal::new(0);
327        let s1 = signal.clone();
328        let s2 = signal.clone();
329        let s3 = signal.clone();
330
331        // Add 3 subscribers (should fit in SmallVec without heap allocation)
332        let _e1 = create_effect(move || {
333            let _ = s1.get();
334        });
335        let _e2 = create_effect(move || {
336            let _ = s2.get();
337        });
338        let _e3 = create_effect(move || {
339            let _ = s3.get();
340        });
341
342        // Verify subscribers are tracked
343        assert_eq!(signal.subscribers.borrow().len(), 3);
344    }
345}