Skip to main content

ferroui_base/rendering/composition/transport/
batch_stream_array_pool.rs

1use std::marker::PhantomData;
2use std::ops::Deref;
3use std::sync::{Mutex, MutexGuard, PoisonError};
4
5const USAGE_STATISTICS_LENGTH: usize = 10;
6
7/// How the items of a [`BatchStreamPoolBase`] are created, cleared and
8/// destroyed (the abstract and virtual members of the C# base class).
9pub trait IBatchStreamPoolItems<T>: Send + Sync {
10    /// Creates a new item; called when the pool has none to hand out.
11    fn create_item(&self) -> T;
12
13    /// Clears an item that is being returned to the pool.
14    fn clear_item(&self, _item: &mut T) {}
15
16    /// Destroys an item the pool does not keep.
17    fn destroy_item(&self, _item: T) {}
18}
19
20/// The callback that asks the owner of a pool to start its update timer.
21pub type BatchStreamPoolStartTimer = Box<dyn Fn() + Send + Sync>;
22
23struct PoolState<T> {
24    pool: Vec<T>,
25    disposed: bool,
26    usage: i32,
27    usage_statistics: [i32; USAGE_STATISTICS_LENGTH],
28    usage_statistics_slot: usize,
29    timer_is_running: bool,
30    current_update_tick: u64,
31    last_activity_tick: u64,
32}
33
34/// A pool that keeps a number of elements that was used in the last 10 seconds
35///
36/// # Trimming
37///
38/// The C# class trims itself from a one second timer that it starts on the
39/// dispatcher of the thread it was created on (or through a `startTimer`
40/// delegate that receives the timer procedure). This port has no timer of
41/// its own; the owner drives it instead:
42///
43/// * while [`timer_is_running`](Self::timer_is_running) is `true`, the owner
44///   calls [`update_timer_tick`](Self::update_timer_tick) once per second;
45/// * when `update_timer_tick` returns `false` the timer has stopped (the
46///   pool saw no activity for 21 ticks) and the owner stops calling it;
47/// * the timer starts again on the next [`get`](Self::get) or
48///   [`return_item`](Self::return_item). The optional `start_timer` callback
49///   given to the constructor is invoked each time that happens (and once
50///   from the constructor), so the owner does not have to poll
51///   `timer_is_running`. The callback is invoked on the thread that used the
52///   pool, outside of the pool lock.
53///
54/// A pool created with `reclaim_immediately` never keeps returned items and
55/// never asks for a timer.
56pub struct BatchStreamPoolBase<T, I: IBatchStreamPoolItems<T>> {
57    items: I,
58    start_timer: Option<BatchStreamPoolStartTimer>,
59    reclaim_immediately: bool,
60    state: Mutex<PoolState<T>>,
61}
62
63impl<T, I: IBatchStreamPoolItems<T>> BatchStreamPoolBase<T, I> {
64    pub fn new(items: I, reclaim_immediately: bool, start_timer: Option<BatchStreamPoolStartTimer>) -> Self {
65        let pool = Self {
66            items,
67            start_timer,
68            reclaim_immediately,
69            state: Mutex::new(PoolState {
70                pool: Vec::new(),
71                disposed: false,
72                usage: 0,
73                usage_statistics: [0; USAGE_STATISTICS_LENGTH],
74                usage_statistics_slot: 0,
75                timer_is_running: false,
76                current_update_tick: 0,
77                last_activity_tick: 0,
78            }),
79        };
80        let start = pool.ensure_update_timer(&mut pool.lock());
81        pool.notify_start_timer(start);
82        pool
83    }
84
85    fn lock(&self) -> MutexGuard<'_, PoolState<T>> {
86        self.state.lock().unwrap_or_else(PoisonError::into_inner)
87    }
88
89    /// The number of items currently handed out.
90    pub fn current_usage(&self) -> i32 {
91        self.lock().usage
92    }
93
94    /// The number of items currently kept in the pool.
95    pub fn current_pool(&self) -> i32 {
96        self.lock().pool.len() as i32
97    }
98
99    /// Whether the owner is expected to call
100    /// [`update_timer_tick`](Self::update_timer_tick) once per second.
101    pub fn timer_is_running(&self) -> bool {
102        self.lock().timer_is_running
103    }
104
105    /// Marks the timer as running when it is needed and is not running yet.
106    /// Returns whether the owner has to be asked to start it.
107    fn ensure_update_timer(&self, state: &mut PoolState<T>) -> bool {
108        if state.timer_is_running || !self.needs_timer(state) {
109            return false;
110        }
111
112        state.timer_is_running = true;
113        true
114    }
115
116    fn notify_start_timer(&self, start: bool) {
117        if start {
118            if let Some(start_timer) = &self.start_timer {
119                start_timer();
120            }
121        }
122    }
123
124    fn needs_timer(&self, state: &PoolState<T>) -> bool {
125        !self.reclaim_immediately
126            && state.current_update_tick.wrapping_sub(state.last_activity_tick)
127                < USAGE_STATISTICS_LENGTH as u64 * 2 + 1
128    }
129
130    /// The timer procedure: trims the pool down to the number of items that
131    /// were in use at the same time during the last 10 ticks (but keeps at
132    /// least 10), and advances the usage statistics.
133    ///
134    /// To be called by the owner once per second while the timer is running.
135    /// Returns whether the timer keeps running.
136    pub fn update_timer_tick(&self) -> bool {
137        let mut state = self.lock();
138        state.current_update_tick = state.current_update_tick.wrapping_add(1);
139        let maximum_usage = state.usage_statistics.iter().copied().max().unwrap_or(0);
140        let recently_used_pooled_slots = maximum_usage - state.usage;
141        let keep_slots = recently_used_pooled_slots.max(10);
142        while (keep_slots as usize) < state.pool.len() {
143            if let Some(item) = state.pool.pop() {
144                self.items.destroy_item(item);
145            }
146        }
147
148        state.usage_statistics_slot = (state.usage_statistics_slot + 1) % USAGE_STATISTICS_LENGTH;
149        let slot = state.usage_statistics_slot;
150        state.usage_statistics[slot] = 0;
151
152        state.timer_is_running = self.needs_timer(&state);
153        state.timer_is_running
154    }
155
156    fn on_activity(&self, state: &mut PoolState<T>) -> bool {
157        state.last_activity_tick = state.current_update_tick;
158        self.ensure_update_timer(state)
159    }
160
161    /// Takes an item from the pool, creating one when the pool is empty.
162    pub fn get(&self) -> T {
163        let (item, start) = {
164            let mut state = self.lock();
165            state.usage += 1;
166            let slot = state.usage_statistics_slot;
167            if state.usage_statistics[slot] < state.usage {
168                state.usage_statistics[slot] = state.usage;
169            }
170
171            let start = self.on_activity(&mut state);
172
173            (state.pool.pop(), start)
174        };
175        self.notify_start_timer(start);
176
177        match item {
178            Some(item) => item,
179            None => self.items.create_item(),
180        }
181    }
182
183    /// Gives an item back (C# `Return`). The item is cleared, and then kept
184    /// for reuse unless the pool is disposed or reclaims immediately.
185    pub fn return_item(&self, mut item: T) {
186        self.items.clear_item(&mut item);
187        let result = {
188            let mut state = self.lock();
189            state.usage -= 1;
190            if !state.disposed && !self.reclaim_immediately {
191                state.pool.push(item);
192                Ok(self.on_activity(&mut state))
193            } else {
194                Err(item)
195            }
196        };
197
198        match result {
199            Ok(start) => self.notify_start_timer(start),
200            Err(item) => self.items.destroy_item(item),
201        }
202    }
203
204    /// Destroys the pooled items; items returned from now on are destroyed
205    /// instead of being kept.
206    pub fn dispose(&self) {
207        let mut state = self.lock();
208        state.disposed = true;
209        for item in state.pool.drain(..) {
210            self.items.destroy_item(item);
211        }
212    }
213}
214
215impl<T, I: IBatchStreamPoolItems<T>> Drop for BatchStreamPoolBase<T, I> {
216    fn drop(&mut self) {
217        self.dispose();
218    }
219}
220
221/// Creates and clears the arrays of a [`BatchStreamObjectPool`].
222pub struct BatchStreamObjectPoolItems<T> {
223    array_size: usize,
224    marker: PhantomData<fn() -> T>,
225}
226
227impl<T> IBatchStreamPoolItems<Box<[Option<T>]>> for BatchStreamObjectPoolItems<T> {
228    fn create_item(&self) -> Box<[Option<T>]> {
229        std::iter::repeat_with(|| None).take(self.array_size).collect()
230    }
231
232    fn clear_item(&self, item: &mut Box<[Option<T>]>) {
233        item.fill_with(|| None);
234    }
235}
236
237/// A pool of fixed-size arrays of optional object references.
238pub struct BatchStreamObjectPool<T> {
239    base: BatchStreamPoolBase<Box<[Option<T>]>, BatchStreamObjectPoolItems<T>>,
240}
241
242impl<T> BatchStreamObjectPool<T> {
243    /// The array size used when none is specified.
244    pub const DEFAULT_ARRAY_SIZE: usize = 128;
245
246    pub fn new(reclaim_immediately: bool, array_size: usize, start_timer: Option<BatchStreamPoolStartTimer>) -> Self {
247        Self {
248            base: BatchStreamPoolBase::new(
249                BatchStreamObjectPoolItems { array_size, marker: PhantomData },
250                reclaim_immediately,
251                start_timer,
252            ),
253        }
254    }
255
256    pub fn array_size(&self) -> usize {
257        self.base.items.array_size
258    }
259}
260
261impl<T> Deref for BatchStreamObjectPool<T> {
262    type Target = BatchStreamPoolBase<Box<[Option<T>]>, BatchStreamObjectPoolItems<T>>;
263
264    fn deref(&self) -> &Self::Target {
265        &self.base
266    }
267}
268
269/// Creates the buffers of a [`BatchStreamMemoryPool`].
270pub struct BatchStreamMemoryPoolItems {
271    buffer_size: usize,
272}
273
274impl IBatchStreamPoolItems<Box<[u8]>> for BatchStreamMemoryPoolItems {
275    fn create_item(&self) -> Box<[u8]> {
276        vec![0u8; self.buffer_size].into_boxed_slice()
277    }
278}
279
280/// A pool of fixed-size byte buffers.
281///
282/// Buffers are zeroed when they are created; like the unmanaged blocks of
283/// the C# class they are not cleared when they are returned.
284pub struct BatchStreamMemoryPool {
285    base: BatchStreamPoolBase<Box<[u8]>, BatchStreamMemoryPoolItems>,
286}
287
288impl BatchStreamMemoryPool {
289    /// The buffer size used when none is specified.
290    pub const DEFAULT_BUFFER_SIZE: usize = 1024;
291
292    pub fn new(reclaim_immediately: bool, buffer_size: usize, start_timer: Option<BatchStreamPoolStartTimer>) -> Self {
293        Self {
294            base: BatchStreamPoolBase::new(
295                BatchStreamMemoryPoolItems { buffer_size },
296                reclaim_immediately,
297                start_timer,
298            ),
299        }
300    }
301
302    pub fn buffer_size(&self) -> usize {
303        self.base.items.buffer_size
304    }
305}
306
307impl Deref for BatchStreamMemoryPool {
308    type Target = BatchStreamPoolBase<Box<[u8]>, BatchStreamMemoryPoolItems>;
309
310    fn deref(&self) -> &Self::Target {
311        &self.base
312    }
313}
314
315#[cfg(test)]
316mod tests {
317    use super::*;
318    use std::sync::atomic::{AtomicUsize, Ordering};
319    use std::sync::Arc;
320
321    fn assert_send_sync<T: Send + Sync>() {}
322
323    #[test]
324    fn pools_are_send_and_sync() {
325        assert_send_sync::<BatchStreamMemoryPool>();
326        assert_send_sync::<BatchStreamObjectPool<Box<dyn std::any::Any + Send>>>();
327        assert_send_sync::<BatchStreamObjectPool<Arc<str>>>();
328    }
329
330    /// Counts created and destroyed items.
331    #[derive(Default)]
332    struct Counting {
333        created: AtomicUsize,
334        cleared: AtomicUsize,
335        destroyed: AtomicUsize,
336    }
337
338    impl IBatchStreamPoolItems<usize> for Arc<Counting> {
339        fn create_item(&self) -> usize {
340            self.created.fetch_add(1, Ordering::SeqCst)
341        }
342        fn clear_item(&self, _item: &mut usize) {
343            self.cleared.fetch_add(1, Ordering::SeqCst);
344        }
345        fn destroy_item(&self, _item: usize) {
346            self.destroyed.fetch_add(1, Ordering::SeqCst);
347        }
348    }
349
350    fn counting_pool(reclaim_immediately: bool) -> (BatchStreamPoolBase<usize, Arc<Counting>>, Arc<Counting>) {
351        let counting = Arc::new(Counting::default());
352        (BatchStreamPoolBase::new(counting.clone(), reclaim_immediately, None), counting)
353    }
354
355    #[test]
356    fn get_creates_and_return_pools_items() {
357        let (pool, counting) = counting_pool(false);
358        let a = pool.get();
359        let b = pool.get();
360        assert_eq!((0, 1), (a, b));
361        assert_eq!(2, pool.current_usage());
362        assert_eq!(0, pool.current_pool());
363
364        pool.return_item(a);
365        assert_eq!(1, pool.current_usage());
366        assert_eq!(1, pool.current_pool());
367        assert_eq!(1, counting.cleared.load(Ordering::SeqCst));
368
369        // The pooled item is reused (last in, first out) instead of creating one.
370        pool.return_item(b);
371        assert_eq!(1, pool.get());
372        assert_eq!(0, pool.get());
373        assert_eq!(2, counting.created.load(Ordering::SeqCst));
374        assert_eq!(0, counting.destroyed.load(Ordering::SeqCst));
375    }
376
377    #[test]
378    fn reclaim_immediately_destroys_returned_items_and_needs_no_timer() {
379        let (pool, counting) = counting_pool(true);
380        assert!(!pool.timer_is_running());
381        let a = pool.get();
382        pool.return_item(a);
383        assert_eq!(0, pool.current_usage());
384        assert_eq!(0, pool.current_pool());
385        assert_eq!(1, counting.cleared.load(Ordering::SeqCst));
386        assert_eq!(1, counting.destroyed.load(Ordering::SeqCst));
387        assert!(!pool.timer_is_running());
388        assert!(!pool.update_timer_tick());
389    }
390
391    #[test]
392    fn dispose_destroys_pooled_items_and_later_returns() {
393        let (pool, counting) = counting_pool(false);
394        let items: Vec<usize> = (0..3).map(|_| pool.get()).collect();
395        pool.return_item(items[0]);
396        pool.return_item(items[1]);
397        pool.dispose();
398        assert_eq!(0, pool.current_pool());
399        assert_eq!(2, counting.destroyed.load(Ordering::SeqCst));
400
401        pool.return_item(items[2]);
402        assert_eq!(0, pool.current_pool());
403        assert_eq!(0, pool.current_usage());
404        assert_eq!(3, counting.destroyed.load(Ordering::SeqCst));
405    }
406
407    #[test]
408    fn dropping_the_pool_destroys_pooled_items() {
409        let (pool, counting) = counting_pool(false);
410        let a = pool.get();
411        pool.return_item(a);
412        drop(pool);
413        assert_eq!(1, counting.destroyed.load(Ordering::SeqCst));
414    }
415
416    #[test]
417    fn tick_keeps_at_least_ten_items() {
418        let (pool, counting) = counting_pool(false);
419        let items: Vec<usize> = (0..8).map(|_| pool.get()).collect();
420        for item in items {
421            pool.return_item(item);
422        }
423        for _ in 0..30 {
424            pool.update_timer_tick();
425        }
426        assert_eq!(8, pool.current_pool());
427        assert_eq!(0, counting.destroyed.load(Ordering::SeqCst));
428    }
429
430    #[test]
431    fn tick_trims_to_the_peak_usage_of_the_last_ten_ticks() {
432        let (pool, counting) = counting_pool(false);
433        // Peak usage of 30, all returned.
434        let items: Vec<usize> = (0..30).map(|_| pool.get()).collect();
435        for item in items {
436            pool.return_item(item);
437        }
438        assert_eq!(30, pool.current_pool());
439
440        // The peak stays in the statistics for 10 ticks (the slot it was
441        // recorded in is the last one to be overwritten).
442        for _ in 0..9 {
443            assert!(pool.update_timer_tick());
444            assert_eq!(30, pool.current_pool());
445        }
446        // The 10th tick still sees the peak before clearing its slot ...
447        assert!(pool.update_timer_tick());
448        assert_eq!(30, pool.current_pool());
449        // ... and the 11th trims down to the minimum of 10.
450        assert!(pool.update_timer_tick());
451        assert_eq!(10, pool.current_pool());
452        assert_eq!(20, counting.destroyed.load(Ordering::SeqCst));
453    }
454
455    #[test]
456    fn items_in_use_count_against_the_slots_to_keep() {
457        let (pool, _counting) = counting_pool(false);
458        // Peak usage of 40; 25 stay in use, 15 are pooled.
459        let mut items: Vec<usize> = (0..40).map(|_| pool.get()).collect();
460        for item in items.drain(25..) {
461            pool.return_item(item);
462        }
463        assert_eq!(15, pool.current_pool());
464        // keep = max(40 - 25, 10) = 15: nothing to trim.
465        pool.update_timer_tick();
466        assert_eq!(15, pool.current_pool());
467
468        // 15 more are taken from the pool and returned: usage peaks at 40 again.
469        let more: Vec<usize> = (0..15).map(|_| pool.get()).collect();
470        assert_eq!(0, pool.current_pool());
471        for item in more {
472            pool.return_item(item);
473        }
474        // Return everything: 40 pooled, usage 0, recent peak 40 -> keep 40.
475        for item in items {
476            pool.return_item(item);
477        }
478        pool.update_timer_tick();
479        assert_eq!(40, pool.current_pool());
480    }
481
482    #[test]
483    fn timer_stops_after_21_idle_ticks_and_restarts_on_activity() {
484        let starts = Arc::new(AtomicUsize::new(0));
485        let counting = Arc::new(Counting::default());
486        let pool = {
487            let starts = starts.clone();
488            BatchStreamPoolBase::new(
489                counting,
490                false,
491                Some(Box::new(move || {
492                    starts.fetch_add(1, Ordering::SeqCst);
493                })),
494            )
495        };
496        // The timer is requested from the constructor.
497        assert_eq!(1, starts.load(Ordering::SeqCst));
498        assert!(pool.timer_is_running());
499
500        // Activity while the timer runs does not request it again.
501        let item = pool.get();
502        pool.return_item(item);
503        assert_eq!(1, starts.load(Ordering::SeqCst));
504
505        for _ in 0..20 {
506            assert!(pool.update_timer_tick());
507        }
508        assert!(!pool.update_timer_tick());
509        assert!(!pool.timer_is_running());
510
511        let item = pool.get();
512        assert_eq!(2, starts.load(Ordering::SeqCst));
513        assert!(pool.timer_is_running());
514        pool.return_item(item);
515        assert_eq!(2, starts.load(Ordering::SeqCst));
516
517        // Activity resets the idle countdown.
518        for _ in 0..15 {
519            assert!(pool.update_timer_tick());
520        }
521        let item = pool.get();
522        pool.return_item(item);
523        for _ in 0..20 {
524            assert!(pool.update_timer_tick());
525        }
526        assert!(!pool.update_timer_tick());
527    }
528
529    #[test]
530    fn start_timer_callback_may_use_the_pool() {
531        // The callback runs outside of the pool lock.
532        let pool: Arc<Mutex<Option<Arc<BatchStreamMemoryPool>>>> = Arc::new(Mutex::new(None));
533        let observed = Arc::new(AtomicUsize::new(0));
534        let created = {
535            let pool = pool.clone();
536            let observed = observed.clone();
537            Arc::new(BatchStreamMemoryPool::new(
538                false,
539                16,
540                Some(Box::new(move || {
541                    if let Some(pool) = pool.lock().unwrap().as_ref() {
542                        observed.store(pool.current_usage() as usize, Ordering::SeqCst);
543                    }
544                })),
545            ))
546        };
547        *pool.lock().unwrap() = Some(created.clone());
548        for _ in 0..21 {
549            created.update_timer_tick();
550        }
551        let buffer = created.get();
552        assert_eq!(1, observed.load(Ordering::SeqCst));
553        created.return_item(buffer);
554        *pool.lock().unwrap() = None;
555    }
556
557    #[test]
558    fn object_pool_hands_out_cleared_arrays() {
559        let pool = BatchStreamObjectPool::<Arc<str>>::new(false, BatchStreamObjectPool::<Arc<str>>::DEFAULT_ARRAY_SIZE, None);
560        assert_eq!(128, pool.array_size());
561        let mut array = pool.get();
562        assert_eq!(128, array.len());
563        assert!(array.iter().all(|slot| slot.is_none()));
564
565        let value: Arc<str> = Arc::from("value");
566        array[3] = Some(value.clone());
567        assert_eq!(2, Arc::strong_count(&value));
568        pool.return_item(array);
569        // Returning clears the references.
570        assert_eq!(1, Arc::strong_count(&value));
571        assert_eq!(1, pool.current_pool());
572
573        let array = pool.get();
574        assert!(array.iter().all(|slot| slot.is_none()));
575    }
576
577    #[test]
578    fn memory_pool_hands_out_buffers_of_the_requested_size() {
579        let pool = BatchStreamMemoryPool::new(false, BatchStreamMemoryPool::DEFAULT_BUFFER_SIZE, None);
580        assert_eq!(1024, pool.buffer_size());
581        let mut buffer = pool.get();
582        assert_eq!(1024, buffer.len());
583        buffer[0] = 42;
584        pool.return_item(buffer);
585        // Buffers are not cleared when returned.
586        assert_eq!(42, pool.get()[0]);
587    }
588
589    #[test]
590    fn pool_can_be_shared_between_threads() {
591        let pool = Arc::new(BatchStreamMemoryPool::new(false, 64, None));
592        let threads: Vec<_> = (0..4)
593            .map(|_| {
594                let pool = pool.clone();
595                std::thread::spawn(move || {
596                    for _ in 0..200 {
597                        let buffer = pool.get();
598                        pool.return_item(buffer);
599                    }
600                })
601            })
602            .collect();
603        for thread in threads {
604            thread.join().unwrap();
605        }
606        assert_eq!(0, pool.current_usage());
607        assert!(pool.current_pool() >= 1 && pool.current_pool() <= 4);
608    }
609}