dioxus_motion/animations/
closure_pool.rs

1//! Web closure pooling system for performance optimization
2//!
3//! Provides a pool of reusable JavaScript closures to avoid the overhead
4//! of creating new closures for every animation frame callback.
5
6#[cfg(feature = "web")]
7use std::cell::RefCell;
8#[cfg(feature = "web")]
9use std::collections::{HashMap, HashSet};
10#[cfg(feature = "web")]
11use wasm_bindgen::prelude::*;
12
13/// Pool of reusable JavaScript closures for web platform optimization
14#[cfg(feature = "web")]
15pub struct WebClosurePool {
16    /// Count of available closure slots
17    available_count: usize,
18    /// Set of currently in-use closure IDs
19    in_use_ids: HashSet<u32>,
20    /// Registry mapping closure IDs to their callbacks
21    callback_registry: HashMap<u32, Box<dyn FnOnce() + Send>>,
22    /// Next available closure ID
23    next_id: u32,
24    /// Maximum pool size to maintain
25    max_pool_size: usize,
26}
27
28#[cfg(feature = "web")]
29impl WebClosurePool {
30    /// Creates a new web closure pool
31    pub fn new() -> Self {
32        Self {
33            available_count: 0,
34            in_use_ids: HashSet::new(),
35            callback_registry: HashMap::new(),
36            next_id: 1,
37            max_pool_size: 16, // Reasonable default for most use cases
38        }
39    }
40
41    /// Registers a callback and returns its ID for later execution
42    ///
43    /// # Arguments
44    /// * `callback` - The callback function to execute when requested
45    ///
46    /// # Returns
47    /// The callback ID for later execution
48    pub fn register_callback(&mut self, callback: Box<dyn FnOnce() + Send>) -> u32 {
49        let callback_id = self.next_id;
50        self.next_id += 1;
51
52        // Store the callback in the registry
53        self.callback_registry.insert(callback_id, callback);
54
55        // Mark this closure as in use
56        self.in_use_ids.insert(callback_id);
57
58        // If we had available closures, use one
59        if self.available_count > 0 {
60            self.available_count -= 1;
61        }
62
63        callback_id
64    }
65
66    /// Executes a registered callback and returns the closure to the pool
67    ///
68    /// # Arguments
69    /// * `callback_id` - The ID of the callback to execute
70    pub fn execute_callback(&mut self, callback_id: u32) {
71        if let Some(callback) = self.callback_registry.remove(&callback_id) {
72            callback();
73
74            // Mark closure as no longer in use
75            self.in_use_ids.remove(&callback_id);
76
77            // Return to available pool if we haven't exceeded max size
78            if self.available_count < self.max_pool_size {
79                self.available_count += 1;
80            }
81        }
82    }
83
84    /// Creates a JavaScript closure that will execute the callback with the given ID
85    ///
86    /// # Arguments
87    /// * `callback_id` - The ID of the callback to execute
88    ///
89    /// # Returns
90    /// A JavaScript closure that can be used with web APIs
91    pub fn create_js_closure(&self, callback_id: u32) -> Closure<dyn FnMut()> {
92        Closure::new(move || {
93            // Execute the callback through the global pool
94            execute_and_return_pooled_closure(callback_id);
95        })
96    }
97
98    /// Gets the number of available closures in the pool
99    pub fn available_count(&self) -> usize {
100        self.available_count
101    }
102
103    /// Gets the number of closures currently in use
104    pub fn in_use_count(&self) -> usize {
105        self.in_use_ids.len()
106    }
107
108    /// Clears all closures from the pool
109    pub fn clear(&mut self) {
110        self.available_count = 0;
111        self.in_use_ids.clear();
112        self.callback_registry.clear();
113    }
114}
115
116#[cfg(feature = "web")]
117impl Default for WebClosurePool {
118    fn default() -> Self {
119        Self::new()
120    }
121}
122
123#[cfg(feature = "web")]
124thread_local! {
125    /// Thread-local storage for the global closure pool
126    static CLOSURE_POOL: RefCell<WebClosurePool> = RefCell::new(WebClosurePool::new());
127}
128
129/// Registers a callback in the global pool and returns its ID
130#[cfg(feature = "web")]
131pub fn register_pooled_callback(callback: Box<dyn FnOnce() + Send>) -> u32 {
132    CLOSURE_POOL.with(|pool| {
133        let mut pool = pool.borrow_mut();
134        pool.register_callback(callback)
135    })
136}
137
138/// Creates a JavaScript closure for the given callback ID
139#[cfg(feature = "web")]
140pub fn create_pooled_closure(callback_id: u32) -> Closure<dyn FnMut()> {
141    CLOSURE_POOL.with(|pool| {
142        let pool = pool.borrow();
143        pool.create_js_closure(callback_id)
144    })
145}
146
147/// Executes and returns a closure to the global pool
148#[cfg(feature = "web")]
149pub fn execute_and_return_pooled_closure(closure_id: u32) {
150    CLOSURE_POOL.with(|pool| {
151        let mut pool = pool.borrow_mut();
152        pool.execute_callback(closure_id);
153    });
154}
155
156/// Gets statistics about the global closure pool
157#[cfg(feature = "web")]
158pub fn closure_pool_stats() -> (usize, usize) {
159    CLOSURE_POOL.with(|pool| {
160        let pool = pool.borrow();
161        (pool.available_count(), pool.in_use_count())
162    })
163}
164
165// Stub implementations for non-web platforms
166#[cfg(not(feature = "web"))]
167pub fn register_pooled_callback(_callback: Box<dyn FnOnce() + Send>) -> u32 {
168    0
169}
170
171#[cfg(not(feature = "web"))]
172pub fn execute_and_return_pooled_closure(_closure_id: u32) {}
173
174#[cfg(not(feature = "web"))]
175pub fn closure_pool_stats() -> (usize, usize) {
176    (0, 0)
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    #[cfg(feature = "web")]
184    #[test]
185    fn test_closure_pool_creation() {
186        let pool = WebClosurePool::new();
187        assert_eq!(pool.available_count(), 0);
188        assert_eq!(pool.in_use_count(), 0);
189    }
190
191    #[cfg(feature = "web")]
192    #[test]
193    fn test_callback_registration() {
194        let mut pool = WebClosurePool::new();
195
196        // Test registering a callback
197        let callback = Box::new(|| {});
198        let id = pool.register_callback(callback);
199        assert!(id > 0);
200
201        // Test executing a callback
202        pool.execute_callback(id);
203
204        // Callback should be removed after execution
205        pool.execute_callback(id); // Should not panic
206    }
207
208    #[cfg(feature = "web")]
209    #[test]
210    fn test_multiple_callbacks() {
211        let mut pool = WebClosurePool::new();
212
213        // Register multiple callbacks
214        let callback1 = Box::new(|| {});
215        let callback2 = Box::new(|| {});
216        let id1 = pool.register_callback(callback1);
217        let id2 = pool.register_callback(callback2);
218
219        // IDs should be different
220        assert_ne!(id1, id2);
221
222        // Execute callbacks
223        pool.execute_callback(id1);
224        pool.execute_callback(id2);
225    }
226
227    #[cfg(feature = "web")]
228    #[test]
229    fn test_closure_pool_clear() {
230        let mut pool = WebClosurePool::new();
231
232        // Add some callbacks
233        let callback1 = Box::new(|| {});
234        let callback2 = Box::new(|| {});
235        let _id1 = pool.register_callback(callback1);
236        let _id2 = pool.register_callback(callback2);
237
238        // Clear the pool
239        pool.clear();
240        assert_eq!(pool.available_count(), 0);
241        assert_eq!(pool.in_use_count(), 0);
242    }
243
244    #[test]
245    fn test_non_web_stubs() {
246        // Test that non-web stubs work without panicking
247        #[cfg(not(feature = "web"))]
248        {
249            let callback = Box::new(|| {});
250            let id = register_pooled_callback(callback);
251            execute_and_return_pooled_closure(id);
252            let (available, in_use) = closure_pool_stats();
253            assert_eq!(available, 0);
254            assert_eq!(in_use, 0);
255        }
256
257        // Test that web implementation works correctly
258        #[cfg(feature = "web")]
259        {
260            // Clear the pool first to ensure clean state
261            CLOSURE_POOL.with(|pool| {
262                pool.borrow_mut().clear();
263            });
264
265            let callback = Box::new(|| {});
266            let id = register_pooled_callback(callback);
267
268            // After registration, should have 1 in use
269            let (_available, in_use) = closure_pool_stats();
270            assert_eq!(in_use, 1);
271
272            // After execution, should be returned to available pool
273            execute_and_return_pooled_closure(id);
274            let (available, in_use) = closure_pool_stats();
275            assert_eq!(available, 1);
276            assert_eq!(in_use, 0);
277        }
278    }
279}