Skip to main content

oxigdal_algorithms/vector/
pool.rs

1//! Object pooling for geometry types to reduce allocations
2//!
3//! This module provides thread-local object pools for frequently allocated
4//! geometry types (Point, LineString, Polygon). Object pooling can significantly
5//! reduce allocation overhead in batch spatial operations.
6//!
7//! # Performance Benefits
8//!
9//! When performing many spatial operations in sequence, object pooling can:
10//! - Reduce allocations by 2-3x for batch operations
11//! - Decrease GC pressure and memory fragmentation
12//! - Improve cache locality for frequently reused objects
13//!
14//! # Thread Safety
15//!
16//! All pools are thread-local, eliminating synchronization overhead.
17//! Each thread maintains its own independent pools.
18//!
19//! # Usage Example
20//!
21//! ```
22//! use oxigdal_algorithms::vector::{Point, buffer_point_pooled, BufferOptions};
23//!
24//! let point = Point::new(0.0, 0.0);
25//! let options = BufferOptions::default();
26//!
27//! // Get a pooled polygon - automatically returned to pool when guard drops
28//! let buffered = buffer_point_pooled(&point, 10.0, &options)?;
29//! // Use the buffered geometry...
30//! // Automatically returned to pool here
31//! # Ok::<(), oxigdal_algorithms::error::AlgorithmError>(())
32//! ```
33
34use oxigdal_core::vector::{Coordinate, LineString, Point, Polygon};
35use std::cell::RefCell;
36use std::mem::ManuallyDrop;
37use std::ops::{Deref, DerefMut};
38
39/// Initial capacity for each object pool
40const INITIAL_POOL_CAPACITY: usize = 16;
41
42/// Maximum number of objects to keep in pool to avoid unbounded growth
43const MAX_POOL_SIZE: usize = 128;
44
45/// Generic object pool for reusable objects
46///
47/// The pool maintains a collection of reusable objects to reduce allocation
48/// overhead. Objects are returned to the pool automatically via `PoolGuard`.
49pub struct Pool<T> {
50    objects: Vec<T>,
51    capacity: usize,
52}
53
54impl<T> Pool<T> {
55    /// Creates a new empty pool with the given capacity
56    pub fn new(capacity: usize) -> Self {
57        Self {
58            objects: Vec::with_capacity(capacity),
59            capacity,
60        }
61    }
62
63    /// Gets an object from the pool, or creates a new one if pool is empty
64    pub fn get<F>(&mut self, create: F) -> T
65    where
66        F: FnOnce() -> T,
67    {
68        self.objects.pop().unwrap_or_else(create)
69    }
70
71    /// Returns an object to the pool for reuse
72    ///
73    /// If the pool is at maximum capacity, the object is dropped instead
74    pub fn put(&mut self, obj: T) {
75        if self.objects.len() < MAX_POOL_SIZE {
76            self.objects.push(obj);
77        }
78        // Otherwise drop the object to prevent unbounded growth
79    }
80
81    /// Clears all objects from the pool
82    pub fn clear(&mut self) {
83        self.objects.clear();
84    }
85
86    /// Returns the number of objects currently in the pool
87    pub fn len(&self) -> usize {
88        self.objects.len()
89    }
90
91    /// Returns true if the pool is empty
92    pub fn is_empty(&self) -> bool {
93        self.objects.is_empty()
94    }
95}
96
97impl<T> Default for Pool<T> {
98    fn default() -> Self {
99        Self::new(INITIAL_POOL_CAPACITY)
100    }
101}
102
103/// RAII guard that returns an object to the pool when dropped
104///
105/// This ensures objects are automatically returned to the pool even if
106/// an error occurs or early return happens.
107///
108/// The guard uses `ManuallyDrop` internally to avoid double-drop issues
109/// when converting to the inner value.
110pub struct PoolGuard<'a, T> {
111    object: ManuallyDrop<T>,
112    pool: &'a RefCell<Pool<T>>,
113    /// Track if into_inner() was called to prevent double-drop
114    consumed: bool,
115}
116
117impl<'a, T> PoolGuard<'a, T> {
118    /// Creates a new pool guard
119    fn new(object: T, pool: &'a RefCell<Pool<T>>) -> Self {
120        Self {
121            object: ManuallyDrop::new(object),
122            pool,
123            consumed: false,
124        }
125    }
126
127    /// Consumes the guard and returns the inner object without returning to pool
128    #[allow(unsafe_code)]
129    pub fn into_inner(mut self) -> T {
130        self.consumed = true;
131        // SAFETY: We take ownership via into_inner, and set consumed flag
132        // to prevent Drop from running
133        unsafe { ManuallyDrop::take(&mut self.object) }
134    }
135}
136
137impl<'a, T> Deref for PoolGuard<'a, T> {
138    type Target = T;
139
140    fn deref(&self) -> &Self::Target {
141        &self.object
142    }
143}
144
145impl<'a, T> DerefMut for PoolGuard<'a, T> {
146    fn deref_mut(&mut self) -> &mut Self::Target {
147        &mut self.object
148    }
149}
150
151impl<'a, T> Drop for PoolGuard<'a, T> {
152    #[allow(unsafe_code)]
153    fn drop(&mut self) {
154        if !self.consumed {
155            // SAFETY: We only take here if not consumed, and we're in Drop
156            // so this is the last time we'll access this object
157            let object = unsafe { ManuallyDrop::take(&mut self.object) };
158            if let Ok(mut pool) = self.pool.try_borrow_mut() {
159                pool.put(object);
160            }
161            // If we can't borrow the pool (shouldn't happen), object is dropped here
162        }
163    }
164}
165
166// Thread-local pools for each geometry type
167thread_local! {
168    static POINT_POOL: RefCell<Pool<Point>> = RefCell::new(Pool::default());
169    static LINESTRING_POOL: RefCell<Pool<LineString>> = RefCell::new(Pool::default());
170    static POLYGON_POOL: RefCell<Pool<Polygon>> = RefCell::new(Pool::default());
171    static COORDINATE_VEC_POOL: RefCell<Pool<Vec<Coordinate>>> = RefCell::new(Pool::default());
172}
173
174/// Gets a Point from the thread-local pool
175///
176/// If the pool is empty, creates a new Point with the given coordinates.
177#[allow(unsafe_code)]
178pub fn get_pooled_point(x: f64, y: f64) -> PoolGuard<'static, Point> {
179    POINT_POOL.with(|pool| {
180        let mut pool_ref = pool.borrow_mut();
181        let mut point = pool_ref.get(|| Point::new(0.0, 0.0));
182        // Update the point coordinates via the public coord field
183        point.coord.x = x;
184        point.coord.y = y;
185        point.coord.z = None;
186        drop(pool_ref);
187        // SAFETY: The RefCell will live for the entire 'static lifetime as it's thread_local
188        // The guard holds a reference that prevents the pool from being dropped while in use
189        PoolGuard::new(point, unsafe { &*(pool as *const RefCell<Pool<Point>>) })
190    })
191}
192
193/// Gets a LineString from the thread-local pool
194///
195/// Returns a LineString with 2 origin coordinates. The caller should replace
196/// the coordinates via `coords_mut()` to set the actual line geometry.
197///
198/// Note: LineStrings require at least 2 points. The returned LineString will
199/// have placeholder coordinates that should be replaced by the caller.
200#[allow(unsafe_code)]
201pub fn get_pooled_linestring() -> PoolGuard<'static, LineString> {
202    LINESTRING_POOL.with(|pool| {
203        let mut pool_ref = pool.borrow_mut();
204        let mut linestring = pool_ref.get(|| {
205            // Create minimal valid linestring (2 points required)
206            // Note: We cannot return Result here, so we use minimal valid geometry
207            // In practice, LineString::new should never fail with valid coordinates
208            match LineString::new(vec![
209                Coordinate::new_2d(0.0, 0.0),
210                Coordinate::new_2d(0.0, 0.0),
211            ]) {
212                Ok(ls) => ls,
213                Err(_) => {
214                    // This should never happen with valid coordinates
215                    // Create with slightly different coords to ensure validity
216                    match LineString::new(vec![
217                        Coordinate::new_2d(0.0, 0.0),
218                        Coordinate::new_2d(1.0, 1.0),
219                    ]) {
220                        Ok(ls) => ls,
221                        Err(_) => {
222                            // Truly should never reach here - coordinates are valid
223                            // Direct construction as last resort (safe because we control the data)
224                            LineString {
225                                coords: vec![
226                                    Coordinate::new_2d(0.0, 0.0),
227                                    Coordinate::new_2d(1.0, 1.0),
228                                ],
229                            }
230                        }
231                    }
232                }
233            }
234        });
235        // Reset to minimal valid state using public field access
236        if linestring.len() < 2 {
237            linestring.coords.clear();
238            linestring.coords.push(Coordinate::new_2d(0.0, 0.0));
239            linestring.coords.push(Coordinate::new_2d(0.0, 0.0));
240        }
241        drop(pool_ref);
242        // SAFETY: The RefCell will live for the entire 'static lifetime as it's thread_local
243        // The guard holds a reference that prevents the pool from being dropped while in use
244        PoolGuard::new(linestring, unsafe {
245            &*(pool as *const RefCell<Pool<LineString>>)
246        })
247    })
248}
249
250/// Gets a Polygon from the thread-local pool
251///
252/// Returns a minimal triangle polygon. The caller should replace the exterior
253/// ring coordinates and add holes as needed via `exterior_mut()` and `holes_mut()`.
254///
255/// Note: Polygons require at least 4 points in the exterior ring (including closing point).
256/// The returned Polygon will have placeholder coordinates that should be replaced.
257#[allow(unsafe_code)]
258pub fn get_pooled_polygon() -> PoolGuard<'static, Polygon> {
259    POLYGON_POOL.with(|pool| {
260        let mut pool_ref = pool.borrow_mut();
261        let mut polygon = pool_ref.get(|| {
262            // Create minimal valid polygon (triangle = 4 points including closing)
263            let ring = match LineString::new(vec![
264                Coordinate::new_2d(0.0, 0.0),
265                Coordinate::new_2d(1.0, 0.0),
266                Coordinate::new_2d(0.0, 1.0),
267                Coordinate::new_2d(0.0, 0.0),
268            ]) {
269                Ok(ls) => ls,
270                Err(_) => {
271                    // This should never happen - coordinates are valid
272                    // Fallback to direct construction
273                    LineString {
274                        coords: vec![
275                            Coordinate::new_2d(0.0, 0.0),
276                            Coordinate::new_2d(1.0, 0.0),
277                            Coordinate::new_2d(0.0, 1.0),
278                            Coordinate::new_2d(0.0, 0.0),
279                        ],
280                    }
281                }
282            };
283            match Polygon::new(ring, vec![]) {
284                Ok(poly) => poly,
285                Err(_) => {
286                    // This should never happen with valid ring
287                    // Direct construction as fallback
288                    Polygon {
289                        exterior: LineString {
290                            coords: vec![
291                                Coordinate::new_2d(0.0, 0.0),
292                                Coordinate::new_2d(1.0, 0.0),
293                                Coordinate::new_2d(0.0, 1.0),
294                                Coordinate::new_2d(0.0, 0.0),
295                            ],
296                        },
297                        interiors: vec![],
298                    }
299                }
300            }
301        });
302        // Reset to minimal valid state using public field access
303        if polygon.exterior.len() < 4 {
304            polygon.exterior.coords.clear();
305            polygon.exterior.coords.push(Coordinate::new_2d(0.0, 0.0));
306            polygon.exterior.coords.push(Coordinate::new_2d(1.0, 0.0));
307            polygon.exterior.coords.push(Coordinate::new_2d(0.0, 1.0));
308            polygon.exterior.coords.push(Coordinate::new_2d(0.0, 0.0));
309        }
310        polygon.interiors.clear();
311        drop(pool_ref);
312        // SAFETY: The RefCell will live for the entire 'static lifetime as it's thread_local
313        // The guard holds a reference that prevents the pool from being dropped while in use
314        PoolGuard::new(polygon, unsafe {
315            &*(pool as *const RefCell<Pool<Polygon>>)
316        })
317    })
318}
319
320/// Gets a `Vec<Coordinate>` from the thread-local pool
321///
322/// The returned vector will be empty and must be populated by the caller.
323#[allow(unsafe_code)]
324pub fn get_pooled_coordinate_vec() -> PoolGuard<'static, Vec<Coordinate>> {
325    COORDINATE_VEC_POOL.with(|pool| {
326        let mut pool_ref = pool.borrow_mut();
327        let mut vec = pool_ref.get(Vec::new);
328        vec.clear();
329        drop(pool_ref);
330        PoolGuard::new(vec, unsafe {
331            &*(pool as *const RefCell<Pool<Vec<Coordinate>>>)
332        })
333    })
334}
335
336/// Clears all thread-local pools
337///
338/// This can be useful for releasing memory after batch operations complete.
339pub fn clear_all_pools() {
340    POINT_POOL.with(|pool| pool.borrow_mut().clear());
341    LINESTRING_POOL.with(|pool| pool.borrow_mut().clear());
342    POLYGON_POOL.with(|pool| pool.borrow_mut().clear());
343    COORDINATE_VEC_POOL.with(|pool| pool.borrow_mut().clear());
344}
345
346/// Gets statistics about pool usage for the current thread
347#[derive(Debug, Clone)]
348pub struct PoolStats {
349    pub points_pooled: usize,
350    pub linestrings_pooled: usize,
351    pub polygons_pooled: usize,
352    pub coordinate_vecs_pooled: usize,
353}
354
355/// Returns statistics about current pool usage
356pub fn get_pool_stats() -> PoolStats {
357    PoolStats {
358        points_pooled: POINT_POOL.with(|pool| pool.borrow().len()),
359        linestrings_pooled: LINESTRING_POOL.with(|pool| pool.borrow().len()),
360        polygons_pooled: POLYGON_POOL.with(|pool| pool.borrow().len()),
361        coordinate_vecs_pooled: COORDINATE_VEC_POOL.with(|pool| pool.borrow().len()),
362    }
363}
364
365#[cfg(test)]
366mod tests {
367    use super::*;
368
369    #[test]
370    fn test_pool_basic_operations() {
371        clear_all_pools();
372
373        let stats = get_pool_stats();
374        assert_eq!(stats.points_pooled, 0);
375
376        // Get a point from the pool
377        {
378            let _point = get_pooled_point(1.0, 2.0);
379            // Point should not be in pool while guard is alive
380            let stats = get_pool_stats();
381            assert_eq!(stats.points_pooled, 0);
382        }
383
384        // Point should be returned to pool after guard drops
385        let stats = get_pool_stats();
386        assert_eq!(stats.points_pooled, 1);
387    }
388
389    #[test]
390    fn test_pool_guard_deref() {
391        clear_all_pools();
392
393        let point = get_pooled_point(3.0, 4.0);
394        assert_eq!(point.x(), 3.0);
395        assert_eq!(point.y(), 4.0);
396    }
397
398    #[test]
399    fn test_pool_guard_deref_mut() {
400        clear_all_pools();
401
402        let mut point = get_pooled_point(1.0, 1.0);
403        point.coord.x = 5.0;
404        point.coord.y = 6.0;
405        assert_eq!(point.x(), 5.0);
406        assert_eq!(point.y(), 6.0);
407    }
408
409    #[test]
410    fn test_pool_reuse() {
411        clear_all_pools();
412
413        // Create and drop several points (each iteration reuses the same point)
414        for i in 0..5 {
415            let _point = get_pooled_point(i as f64, i as f64);
416        }
417
418        // Should have 1 point in the pool (reused across all iterations)
419        let stats = get_pool_stats();
420        assert_eq!(stats.points_pooled, 1);
421
422        // Getting a point should reuse from pool
423        let _point = get_pooled_point(100.0, 100.0);
424        let stats = get_pool_stats();
425        assert_eq!(stats.points_pooled, 0);
426    }
427
428    #[test]
429    fn test_linestring_pool() {
430        clear_all_pools();
431
432        let mut linestring = get_pooled_linestring();
433        linestring.coords.clear();
434        linestring.coords.push(Coordinate::new_2d(0.0, 0.0));
435        linestring.coords.push(Coordinate::new_2d(1.0, 1.0));
436
437        assert_eq!(linestring.len(), 2);
438        drop(linestring);
439
440        let stats = get_pool_stats();
441        assert_eq!(stats.linestrings_pooled, 1);
442
443        // Get another linestring - should be cleared
444        let linestring = get_pooled_linestring();
445        assert_eq!(linestring.len(), 2);
446    }
447
448    #[test]
449    fn test_polygon_pool() {
450        clear_all_pools();
451
452        let polygon = get_pooled_polygon();
453        assert_eq!(polygon.exterior().len(), 4);
454        assert_eq!(polygon.interiors().len(), 0);
455        drop(polygon);
456
457        let stats = get_pool_stats();
458        assert_eq!(stats.polygons_pooled, 1);
459    }
460
461    #[test]
462    fn test_coordinate_vec_pool() {
463        clear_all_pools();
464
465        let mut coords = get_pooled_coordinate_vec();
466        coords.push(Coordinate::new_2d(1.0, 2.0));
467        coords.push(Coordinate::new_2d(3.0, 4.0));
468        assert_eq!(coords.len(), 2);
469        drop(coords);
470
471        let stats = get_pool_stats();
472        assert_eq!(stats.coordinate_vecs_pooled, 1);
473
474        // Get another vec - should be cleared
475        let coords = get_pooled_coordinate_vec();
476        assert_eq!(coords.len(), 0);
477    }
478
479    #[test]
480    fn test_pool_max_size() {
481        clear_all_pools();
482
483        // Add more than MAX_POOL_SIZE objects
484        for i in 0..(MAX_POOL_SIZE + 10) {
485            let _point = get_pooled_point(i as f64, i as f64);
486        }
487
488        // Pool should not exceed max size
489        let stats = get_pool_stats();
490        assert!(stats.points_pooled <= MAX_POOL_SIZE);
491    }
492
493    #[test]
494    fn test_into_inner() {
495        clear_all_pools();
496
497        let guard = get_pooled_point(7.0, 8.0);
498        let point = guard.into_inner();
499
500        assert_eq!(point.x(), 7.0);
501        assert_eq!(point.y(), 8.0);
502
503        // Point should not be returned to pool
504        let stats = get_pool_stats();
505        assert_eq!(stats.points_pooled, 0);
506    }
507
508    #[test]
509    fn test_clear_all_pools() {
510        // Add objects to all pools
511        let _p = get_pooled_point(1.0, 1.0);
512        let _l = get_pooled_linestring();
513        let _poly = get_pooled_polygon();
514        let _coords = get_pooled_coordinate_vec();
515
516        drop(_p);
517        drop(_l);
518        drop(_poly);
519        drop(_coords);
520
521        // Verify pools have objects
522        let stats = get_pool_stats();
523        assert!(stats.points_pooled > 0);
524        assert!(stats.linestrings_pooled > 0);
525        assert!(stats.polygons_pooled > 0);
526        assert!(stats.coordinate_vecs_pooled > 0);
527
528        // Clear all pools
529        clear_all_pools();
530
531        // Verify pools are empty
532        let stats = get_pool_stats();
533        assert_eq!(stats.points_pooled, 0);
534        assert_eq!(stats.linestrings_pooled, 0);
535        assert_eq!(stats.polygons_pooled, 0);
536        assert_eq!(stats.coordinate_vecs_pooled, 0);
537    }
538
539    #[test]
540    fn test_thread_local_isolation() {
541        use std::thread;
542
543        clear_all_pools();
544
545        // Add object to main thread pool
546        {
547            let _point = get_pooled_point(1.0, 1.0);
548        }
549
550        let main_stats = get_pool_stats();
551        assert_eq!(main_stats.points_pooled, 1);
552
553        // Spawn a thread and check its pool is independent
554        let handle = thread::spawn(|| {
555            let stats = get_pool_stats();
556            assert_eq!(stats.points_pooled, 0);
557
558            {
559                let _point = get_pooled_point(2.0, 2.0);
560            }
561
562            let stats = get_pool_stats();
563            assert_eq!(stats.points_pooled, 1);
564        });
565
566        handle.join().expect("Thread panicked");
567
568        // Main thread pool should still have 1 object
569        let main_stats = get_pool_stats();
570        assert_eq!(main_stats.points_pooled, 1);
571    }
572}