playa/core/
debounced_preloader.rs

1//! Debounced preloader - delays full cache preload after attribute changes.
2//!
3//! When attributes change rapidly (e.g., scrubbing sliders), we don't want to
4//! flood the cache with preload requests. Instead:
5//! 1. Immediately render only the current frame
6//! 2. After a configurable delay, trigger full preload radius
7//!
8//! This prevents wasted work and keeps the UI responsive.
9
10use std::time::{Duration, Instant};
11use uuid::Uuid;
12
13/// Debounced preloader for delayed cache warming after attribute changes.
14/// 
15/// # Usage
16/// ```ignore
17/// // On attribute change:
18/// preloader.schedule(comp_uuid);
19/// enqueue_single_frame(current_frame);  // immediate
20/// 
21/// // In update loop:
22/// if let Some(uuid) = preloader.tick() {
23///     enqueue_frame_loads_around_playhead(preload_radius);
24/// }
25/// ```
26#[derive(Debug, Clone)]
27pub struct DebouncedPreloader {
28    /// Delay before triggering full preload
29    delay: Duration,
30    /// Pending preload: (comp_uuid, trigger_time)
31    pending: Option<(Uuid, Instant)>,
32}
33
34impl Default for DebouncedPreloader {
35    fn default() -> Self {
36        Self {
37            delay: Duration::from_millis(500),
38            pending: None,
39        }
40    }
41}
42
43impl DebouncedPreloader {
44    /// Create with custom delay
45    pub fn new(delay_ms: u64) -> Self {
46        Self {
47            delay: Duration::from_millis(delay_ms),
48            pending: None,
49        }
50    }
51
52    /// Set delay duration
53    pub fn set_delay(&mut self, delay_ms: u64) {
54        self.delay = Duration::from_millis(delay_ms);
55    }
56
57    /// Get current delay in milliseconds
58    pub fn delay_ms(&self) -> u64 {
59        self.delay.as_millis() as u64
60    }
61
62    /// Schedule a delayed preload for composition.
63    /// If already pending, resets the timer (debounce behavior).
64    pub fn schedule(&mut self, comp_uuid: Uuid) {
65        let trigger_at = Instant::now() + self.delay;
66        self.pending = Some((comp_uuid, trigger_at));
67        log::trace!(
68            "DebouncedPreloader: scheduled preload for {} in {}ms",
69            comp_uuid,
70            self.delay.as_millis()
71        );
72    }
73
74    /// Cancel any pending preload
75    pub fn cancel(&mut self) {
76        if self.pending.is_some() {
77            log::trace!("DebouncedPreloader: cancelled pending preload");
78        }
79        self.pending = None;
80    }
81
82    /// Check if preload should trigger now.
83    /// Returns Some(comp_uuid) if delay has elapsed, None otherwise.
84    /// Clears the pending state when triggered.
85    pub fn tick(&mut self) -> Option<Uuid> {
86        let Some((uuid, trigger_at)) = self.pending else {
87            return None;
88        };
89
90        if Instant::now() >= trigger_at {
91            self.pending = None;
92            log::trace!("DebouncedPreloader: triggering preload for {}", uuid);
93            Some(uuid)
94        } else {
95            None
96        }
97    }
98
99    /// Check if there's a pending preload
100    pub fn is_pending(&self) -> bool {
101        self.pending.is_some()
102    }
103
104    /// Get pending comp UUID (if any)
105    pub fn pending_comp(&self) -> Option<Uuid> {
106        self.pending.map(|(uuid, _)| uuid)
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn test_immediate_no_trigger() {
116        let mut preloader = DebouncedPreloader::new(100);
117        let uuid = Uuid::new_v4();
118        
119        preloader.schedule(uuid);
120        assert!(preloader.is_pending());
121        
122        // Should not trigger immediately
123        assert!(preloader.tick().is_none());
124    }
125
126    #[test]
127    fn test_trigger_after_delay() {
128        let mut preloader = DebouncedPreloader::new(10); // 10ms
129        let uuid = Uuid::new_v4();
130        
131        preloader.schedule(uuid);
132        std::thread::sleep(Duration::from_millis(15));
133        
134        // Should trigger after delay
135        assert_eq!(preloader.tick(), Some(uuid));
136        assert!(!preloader.is_pending());
137    }
138
139    #[test]
140    fn test_debounce_resets_timer() {
141        let mut preloader = DebouncedPreloader::new(50);
142        let uuid1 = Uuid::new_v4();
143        let uuid2 = Uuid::new_v4();
144        
145        preloader.schedule(uuid1);
146        std::thread::sleep(Duration::from_millis(30));
147        
148        // Re-schedule with different UUID - resets timer
149        preloader.schedule(uuid2);
150        
151        // Should not trigger yet (timer reset)
152        assert!(preloader.tick().is_none());
153        assert_eq!(preloader.pending_comp(), Some(uuid2));
154    }
155}