Skip to main content

oximedia_core/
buffer_pool.rs

1//! Frame buffer pool for zero-copy operations.
2//!
3//! This module provides an ID-based buffer pool (`BufferPool`) for efficient
4//! frame buffer reuse with explicit ownership tracking. Features include:
5//!
6//! - Memory pressure level monitoring (Low / Medium / High / Critical)
7//! - Pressure callbacks fired on level transitions
8//! - Automatic pool shrinking to reclaim idle memory
9//!
10//! # Example
11//!
12//! ```
13//! use oximedia_core::buffer_pool::{BufferPool, PressureThresholds, MemoryPressureLevel};
14//!
15//! let mut pool = BufferPool::with_pressure(4, 1024, PressureThresholds::default());
16//! pool.add_pressure_callback(Box::new(|level| {
17//!     // react to memory pressure level changes
18//!     let _ = level;
19//! }));
20//! // Acquire 3/4 buffers (75%) → High pressure level
21//! let id0 = pool.acquire().expect("buffer available");
22//! let id1 = pool.acquire().expect("buffer available");
23//! let id2 = pool.acquire().expect("buffer available");
24//! assert_eq!(pool.current_pressure_level(), MemoryPressureLevel::High);
25//! pool.release(id0);
26//! pool.release(id1);
27//! pool.release(id2);
28//! assert_eq!(pool.current_pressure_level(), MemoryPressureLevel::Low);
29//! ```
30
31#![allow(dead_code)]
32
33// ─────────────────────────────────────────────────────────────────────────────
34// MemoryPressureLevel
35// ─────────────────────────────────────────────────────────────────────────────
36
37/// Severity level of memory pressure in a buffer pool.
38///
39/// Level is determined by the fraction of buffers that are currently in use:
40///
41/// | Level    | In-use fraction |
42/// |----------|----------------|
43/// | Low      | < medium watermark |
44/// | Medium   | medium ≤ f < high  |
45/// | High     | high ≤ f < critical |
46/// | Critical | ≥ critical watermark |
47#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
48pub enum MemoryPressureLevel {
49    /// Less than the medium watermark is in use — pool is healthy.
50    Low,
51    /// Between medium and high watermarks.
52    Medium,
53    /// Between high and critical watermarks.
54    High,
55    /// At or above the critical watermark — pool nearly exhausted.
56    Critical,
57}
58
59impl Default for MemoryPressureLevel {
60    fn default() -> Self {
61        Self::Low
62    }
63}
64
65// ─────────────────────────────────────────────────────────────────────────────
66// PressureThresholds
67// ─────────────────────────────────────────────────────────────────────────────
68
69/// Fractional watermarks that define memory pressure level transitions.
70///
71/// All values are in the range `[0.0, 1.0]` and represent the fraction of
72/// pool buffers that must be in-use to reach that level.
73#[derive(Debug, Clone, Copy, PartialEq)]
74pub struct PressureThresholds {
75    /// In-use fraction at or above which the level becomes `Medium`. Default 0.5.
76    pub medium_watermark: f64,
77    /// In-use fraction at or above which the level becomes `High`. Default 0.75.
78    pub high_watermark: f64,
79    /// In-use fraction at or above which the level becomes `Critical`. Default 0.9.
80    pub critical_watermark: f64,
81}
82
83impl Default for PressureThresholds {
84    fn default() -> Self {
85        Self {
86            medium_watermark: 0.5,
87            high_watermark: 0.75,
88            critical_watermark: 0.9,
89        }
90    }
91}
92
93impl PressureThresholds {
94    /// Creates custom thresholds.  All values must be in `[0.0, 1.0]` and
95    /// `medium ≤ high ≤ critical`.
96    ///
97    /// # Panics
98    ///
99    /// Panics if values are out of order or outside `[0.0, 1.0]`.
100    #[must_use]
101    pub fn new(medium: f64, high: f64, critical: f64) -> Self {
102        assert!(
103            (0.0..=1.0).contains(&medium)
104                && (0.0..=1.0).contains(&high)
105                && (0.0..=1.0).contains(&critical),
106            "Watermarks must be in [0.0, 1.0]"
107        );
108        assert!(
109            medium <= high && high <= critical,
110            "Watermarks must be in ascending order"
111        );
112        Self {
113            medium_watermark: medium,
114            high_watermark: high,
115            critical_watermark: critical,
116        }
117    }
118
119    /// Maps an in-use fraction to the corresponding [`MemoryPressureLevel`].
120    #[must_use]
121    pub fn level_for_fraction(&self, in_use_fraction: f64) -> MemoryPressureLevel {
122        if in_use_fraction >= self.critical_watermark {
123            MemoryPressureLevel::Critical
124        } else if in_use_fraction >= self.high_watermark {
125            MemoryPressureLevel::High
126        } else if in_use_fraction >= self.medium_watermark {
127            MemoryPressureLevel::Medium
128        } else {
129            MemoryPressureLevel::Low
130        }
131    }
132}
133
134// ─────────────────────────────────────────────────────────────────────────────
135// Callback type alias
136// ─────────────────────────────────────────────────────────────────────────────
137
138/// A callable invoked whenever the pressure level changes.
139///
140/// Receives the new [`MemoryPressureLevel`] as its argument.
141pub type MemoryPressureCallback = Box<dyn Fn(MemoryPressureLevel) + Send + Sync>;
142
143// ─────────────────────────────────────────────────────────────────────────────
144// BufferDesc / PooledBuffer
145// ─────────────────────────────────────────────────────────────────────────────
146
147/// Descriptor for a pooled buffer slot.
148#[derive(Debug, Clone, PartialEq, Eq)]
149pub struct BufferDesc {
150    /// Size of the buffer in bytes.
151    pub size_bytes: usize,
152    /// Required memory alignment in bytes.
153    pub alignment: usize,
154    /// Pool identifier this descriptor belongs to.
155    pub pool_id: u32,
156}
157
158impl BufferDesc {
159    /// Creates a new `BufferDesc`.
160    #[must_use]
161    pub fn new(size_bytes: usize, alignment: usize, pool_id: u32) -> Self {
162        Self {
163            size_bytes,
164            alignment,
165            pool_id,
166        }
167    }
168
169    /// Returns `true` if the alignment equals 4096 (one memory page).
170    #[must_use]
171    pub fn is_page_aligned(&self) -> bool {
172        self.alignment == 4096
173    }
174
175    /// Returns how many slots of `slot_size` bytes are needed to hold this buffer.
176    ///
177    /// # Panics
178    ///
179    /// Panics if `slot_size` is zero.
180    #[must_use]
181    pub fn slots_needed(&self, slot_size: usize) -> usize {
182        assert!(slot_size > 0, "slot_size must be non-zero");
183        self.size_bytes.div_ceil(slot_size)
184    }
185}
186
187/// A buffer managed by the pool with an associated unique ID.
188#[derive(Debug)]
189pub struct PooledBuffer {
190    /// Unique identifier for this buffer within the pool.
191    pub id: u64,
192    /// Raw buffer data.
193    pub data: Vec<u8>,
194    /// Descriptor for this buffer.
195    pub desc: BufferDesc,
196    /// Whether this buffer is currently in use.
197    pub in_use: bool,
198}
199
200impl PooledBuffer {
201    /// Creates a new `PooledBuffer`.
202    #[must_use]
203    pub fn new(id: u64, desc: BufferDesc) -> Self {
204        let data = vec![0u8; desc.size_bytes];
205        Self {
206            id,
207            data,
208            desc,
209            in_use: false,
210        }
211    }
212
213    /// Resets the buffer: zeroes the data and marks it as not in use.
214    pub fn reset(&mut self) {
215        self.data.fill(0);
216        self.in_use = false;
217    }
218
219    /// Returns the number of bytes available (equal to the buffer size).
220    #[must_use]
221    pub fn available_size(&self) -> usize {
222        self.data.len()
223    }
224}
225
226// ─────────────────────────────────────────────────────────────────────────────
227// BufferPool
228// ─────────────────────────────────────────────────────────────────────────────
229
230/// A pool of frame buffers identified by integer IDs.
231///
232/// Buffers are acquired by ID and released back to the pool by ID.
233/// When constructed with [`BufferPool::with_pressure`], the pool monitors
234/// its fill ratio and fires registered callbacks on level transitions.
235pub struct BufferPool {
236    /// Managed buffers.
237    pub buffers: Vec<PooledBuffer>,
238    /// Counter for assigning unique IDs to new buffers.
239    pub next_id: u64,
240    /// Pressure thresholds configuration (if any).
241    thresholds: Option<PressureThresholds>,
242    /// Last known pressure level (used to detect transitions).
243    last_pressure: MemoryPressureLevel,
244    /// Registered pressure callbacks.
245    pressure_callbacks: Vec<MemoryPressureCallback>,
246}
247
248impl std::fmt::Debug for BufferPool {
249    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
250        f.debug_struct("BufferPool")
251            .field("total", &self.buffers.len())
252            .field("available", &self.available_count())
253            .field("last_pressure", &self.last_pressure)
254            .finish()
255    }
256}
257
258impl BufferPool {
259    /// Creates a new `BufferPool` with `count` buffers each of `buf_size` bytes.
260    ///
261    /// All buffers share `pool_id = 0` and default alignment of 64.
262    /// No pressure thresholds or callbacks are configured.
263    #[must_use]
264    pub fn new(count: usize, buf_size: usize) -> Self {
265        let mut buffers = Vec::with_capacity(count);
266        for id in 0..count as u64 {
267            let desc = BufferDesc::new(buf_size, 64, 0);
268            buffers.push(PooledBuffer::new(id, desc));
269        }
270        Self {
271            buffers,
272            next_id: count as u64,
273            thresholds: None,
274            last_pressure: MemoryPressureLevel::Low,
275            pressure_callbacks: Vec::new(),
276        }
277    }
278
279    /// Creates a `BufferPool` with memory pressure monitoring enabled.
280    ///
281    /// See [`PressureThresholds`] for details on watermark configuration.
282    #[must_use]
283    pub fn with_pressure(count: usize, buf_size: usize, thresholds: PressureThresholds) -> Self {
284        let mut pool = Self::new(count, buf_size);
285        pool.thresholds = Some(thresholds);
286        pool
287    }
288
289    /// Registers a callback to be invoked whenever the pressure level transitions.
290    ///
291    /// The callback receives the new [`MemoryPressureLevel`] as its argument.
292    /// Multiple callbacks may be registered and are called in registration order.
293    pub fn add_pressure_callback(&mut self, cb: MemoryPressureCallback) {
294        self.pressure_callbacks.push(cb);
295    }
296
297    // ── Pressure helpers ─────────────────────────────────────────────────────
298
299    /// Computes the current in-use fraction `[0.0, 1.0]`.
300    #[must_use]
301    fn in_use_fraction(&self) -> f64 {
302        let total = self.buffers.len();
303        if total == 0 {
304            return 0.0;
305        }
306        let in_use = self.buffers.iter().filter(|b| b.in_use).count();
307        in_use as f64 / total as f64
308    }
309
310    /// Returns the current memory pressure level.
311    ///
312    /// If no thresholds are configured, always returns [`MemoryPressureLevel::Low`].
313    #[must_use]
314    pub fn current_pressure_level(&self) -> MemoryPressureLevel {
315        match &self.thresholds {
316            None => MemoryPressureLevel::Low,
317            Some(t) => t.level_for_fraction(self.in_use_fraction()),
318        }
319    }
320
321    /// Fires registered callbacks if the pressure level has changed since the
322    /// last call.  Updates `last_pressure` to the current level.
323    fn notify_pressure(&mut self) {
324        let current = self.current_pressure_level();
325        if current != self.last_pressure {
326            self.last_pressure = current;
327            for cb in &self.pressure_callbacks {
328                cb(current);
329            }
330        }
331    }
332
333    // ── Core operations ───────────────────────────────────────────────────────
334
335    /// Acquires an available buffer and returns its ID.
336    ///
337    /// Returns `None` if no buffer is free.
338    /// Triggers pressure notification after the acquisition.
339    #[must_use]
340    pub fn acquire(&mut self) -> Option<u64> {
341        let acquired = self.buffers.iter_mut().find(|b| !b.in_use).map(|buf| {
342            buf.in_use = true;
343            buf.id
344        });
345        if acquired.is_some() {
346            self.notify_pressure();
347        }
348        acquired
349    }
350
351    /// Releases the buffer with the given `id` back to the pool.
352    ///
353    /// If the ID is not found this is a no-op.
354    /// Triggers pressure notification after the release.
355    pub fn release(&mut self, id: u64) {
356        if let Some(buf) = self.buffers.iter_mut().find(|b| b.id == id) {
357            buf.reset();
358        }
359        self.notify_pressure();
360    }
361
362    // ── Pool shrinking ────────────────────────────────────────────────────────
363
364    /// Removes free (not in-use) buffers until the pool has at most `target_count`
365    /// total buffers.
366    ///
367    /// Only idle buffers are removed; buffers currently in use are never evicted.
368    /// Returns the number of buffers that were removed.
369    ///
370    /// # Example
371    ///
372    /// ```
373    /// use oximedia_core::buffer_pool::BufferPool;
374    ///
375    /// let mut pool = BufferPool::new(8, 64);
376    /// let removed = pool.shrink_to(4);
377    /// assert_eq!(removed, 4);
378    /// assert_eq!(pool.total_count(), 4);
379    /// ```
380    pub fn shrink_to(&mut self, target_count: usize) -> usize {
381        let mut removed = 0usize;
382        // Drain from the back to keep ID ordering stable.
383        let mut i = self.buffers.len();
384        while i > 0 && self.buffers.len() > target_count {
385            i -= 1;
386            if !self.buffers[i].in_use {
387                self.buffers.remove(i);
388                removed += 1;
389            }
390        }
391        if removed > 0 {
392            self.notify_pressure();
393        }
394        removed
395    }
396
397    /// Automatically shrinks the pool when pressure is `Low` and more than half
398    /// of the buffers are idle.
399    ///
400    /// Shrinks down to half of the current total count, rounding up so at least
401    /// one buffer always remains. Returns the number of buffers removed.
402    pub fn auto_shrink(&mut self) -> usize {
403        let current_level = self.current_pressure_level();
404        if current_level != MemoryPressureLevel::Low {
405            return 0;
406        }
407        let total = self.buffers.len();
408        let available = self.available_count();
409        if total == 0 || available <= total / 2 {
410            return 0;
411        }
412        let target = (total / 2).max(1);
413        self.shrink_to(target)
414    }
415
416    // ── Statistics ────────────────────────────────────────────────────────────
417
418    /// Returns the number of buffers not currently in use.
419    #[must_use]
420    pub fn available_count(&self) -> usize {
421        self.buffers.iter().filter(|b| !b.in_use).count()
422    }
423
424    /// Returns the total number of buffers managed by this pool.
425    #[must_use]
426    pub fn total_count(&self) -> usize {
427        self.buffers.len()
428    }
429
430    /// Returns the number of buffers currently in use.
431    #[must_use]
432    pub fn in_use_count(&self) -> usize {
433        self.buffers.iter().filter(|b| b.in_use).count()
434    }
435}
436
437// ─────────────────────────────────────────────────────────────────────────────
438// Tests
439// ─────────────────────────────────────────────────────────────────────────────
440
441#[cfg(test)]
442mod tests {
443    use super::*;
444    use std::sync::{Arc, Mutex};
445
446    // --- BufferDesc tests ---
447
448    #[test]
449    fn test_buffer_desc_new() {
450        let desc = BufferDesc::new(1024, 64, 1);
451        assert_eq!(desc.size_bytes, 1024);
452        assert_eq!(desc.alignment, 64);
453        assert_eq!(desc.pool_id, 1);
454    }
455
456    #[test]
457    fn test_buffer_desc_is_page_aligned_true() {
458        let desc = BufferDesc::new(8192, 4096, 0);
459        assert!(desc.is_page_aligned());
460    }
461
462    #[test]
463    fn test_buffer_desc_is_page_aligned_false() {
464        let desc = BufferDesc::new(8192, 64, 0);
465        assert!(!desc.is_page_aligned());
466    }
467
468    #[test]
469    fn test_buffer_desc_slots_needed_exact() {
470        let desc = BufferDesc::new(1024, 64, 0);
471        assert_eq!(desc.slots_needed(512), 2);
472    }
473
474    #[test]
475    fn test_buffer_desc_slots_needed_round_up() {
476        let desc = BufferDesc::new(1025, 64, 0);
477        assert_eq!(desc.slots_needed(512), 3);
478    }
479
480    #[test]
481    fn test_buffer_desc_slots_needed_single_slot() {
482        let desc = BufferDesc::new(100, 64, 0);
483        assert_eq!(desc.slots_needed(200), 1);
484    }
485
486    // --- PooledBuffer tests ---
487
488    #[test]
489    fn test_pooled_buffer_initial_state() {
490        let desc = BufferDesc::new(256, 64, 0);
491        let buf = PooledBuffer::new(42, desc);
492        assert_eq!(buf.id, 42);
493        assert!(!buf.in_use);
494        assert_eq!(buf.available_size(), 256);
495        assert!(buf.data.iter().all(|&b| b == 0));
496    }
497
498    #[test]
499    fn test_pooled_buffer_reset() {
500        let desc = BufferDesc::new(4, 64, 0);
501        let mut buf = PooledBuffer::new(1, desc);
502        buf.in_use = true;
503        buf.data[0] = 0xFF;
504        buf.reset();
505        assert!(!buf.in_use);
506        assert!(buf.data.iter().all(|&b| b == 0));
507    }
508
509    #[test]
510    fn test_pooled_buffer_available_size() {
511        let desc = BufferDesc::new(512, 64, 0);
512        let buf = PooledBuffer::new(0, desc);
513        assert_eq!(buf.available_size(), 512);
514    }
515
516    // --- BufferPool basic tests ---
517
518    #[test]
519    fn test_pool_new() {
520        let pool = BufferPool::new(4, 1024);
521        assert_eq!(pool.total_count(), 4);
522        assert_eq!(pool.available_count(), 4);
523    }
524
525    #[test]
526    fn test_pool_acquire_returns_id() {
527        let mut pool = BufferPool::new(2, 256);
528        let id = pool.acquire();
529        assert!(id.is_some());
530    }
531
532    #[test]
533    fn test_pool_acquire_exhausts_buffers() {
534        let mut pool = BufferPool::new(2, 256);
535        let _id1 = pool.acquire().expect("acquire should succeed");
536        let _id2 = pool.acquire().expect("acquire should succeed");
537        assert!(pool.acquire().is_none());
538    }
539
540    #[test]
541    fn test_pool_available_count_decrements_on_acquire() {
542        let mut pool = BufferPool::new(3, 64);
543        assert_eq!(pool.available_count(), 3);
544        let _ = pool.acquire();
545        assert_eq!(pool.available_count(), 2);
546        let _ = pool.acquire();
547        assert_eq!(pool.available_count(), 1);
548    }
549
550    #[test]
551    fn test_pool_release_makes_buffer_available() {
552        let mut pool = BufferPool::new(1, 64);
553        let id = pool.acquire().expect("acquire should succeed");
554        assert_eq!(pool.available_count(), 0);
555        pool.release(id);
556        assert_eq!(pool.available_count(), 1);
557    }
558
559    #[test]
560    fn test_pool_release_unknown_id_is_noop() {
561        let mut pool = BufferPool::new(2, 64);
562        let before = pool.available_count();
563        pool.release(999);
564        assert_eq!(pool.available_count(), before);
565    }
566
567    #[test]
568    fn test_pool_total_count_unchanged_after_ops() {
569        let mut pool = BufferPool::new(5, 128);
570        let ids: Vec<u64> = (0..5).filter_map(|_| pool.acquire()).collect();
571        assert_eq!(pool.total_count(), 5);
572        for id in ids {
573            pool.release(id);
574        }
575        assert_eq!(pool.total_count(), 5);
576    }
577
578    // --- PressureThresholds tests ---
579
580    #[test]
581    fn test_pressure_thresholds_default() {
582        let t = PressureThresholds::default();
583        assert_eq!(t.level_for_fraction(0.0), MemoryPressureLevel::Low);
584        assert_eq!(t.level_for_fraction(0.5), MemoryPressureLevel::Medium);
585        assert_eq!(t.level_for_fraction(0.75), MemoryPressureLevel::High);
586        assert_eq!(t.level_for_fraction(0.9), MemoryPressureLevel::Critical);
587        assert_eq!(t.level_for_fraction(1.0), MemoryPressureLevel::Critical);
588    }
589
590    #[test]
591    fn test_pressure_thresholds_custom() {
592        let t = PressureThresholds::new(0.4, 0.6, 0.8);
593        assert_eq!(t.level_for_fraction(0.3), MemoryPressureLevel::Low);
594        assert_eq!(t.level_for_fraction(0.5), MemoryPressureLevel::Medium);
595        assert_eq!(t.level_for_fraction(0.7), MemoryPressureLevel::High);
596        assert_eq!(t.level_for_fraction(0.85), MemoryPressureLevel::Critical);
597    }
598
599    #[test]
600    #[should_panic(expected = "Watermarks must be in ascending order")]
601    fn test_pressure_thresholds_out_of_order_panics() {
602        let _ = PressureThresholds::new(0.8, 0.5, 0.9);
603    }
604
605    // --- Memory pressure level tests ---
606
607    #[test]
608    fn test_pool_initial_pressure_level_low() {
609        let pool = BufferPool::with_pressure(4, 64, PressureThresholds::default());
610        assert_eq!(pool.current_pressure_level(), MemoryPressureLevel::Low);
611    }
612
613    #[test]
614    fn test_pool_pressure_level_increases_with_usage() {
615        let mut pool = BufferPool::with_pressure(4, 64, PressureThresholds::default());
616        // acquire 2 / 4 = 0.5 → Medium
617        let _id0 = pool.acquire();
618        let _id1 = pool.acquire();
619        assert_eq!(pool.current_pressure_level(), MemoryPressureLevel::Medium);
620        // acquire 3 / 4 = 0.75 → High
621        let _id2 = pool.acquire();
622        assert_eq!(pool.current_pressure_level(), MemoryPressureLevel::High);
623        // acquire 4 / 4 = 1.0 → Critical
624        let _id3 = pool.acquire();
625        assert_eq!(pool.current_pressure_level(), MemoryPressureLevel::Critical);
626    }
627
628    #[test]
629    fn test_pool_pressure_level_decreases_on_release() {
630        let mut pool = BufferPool::with_pressure(4, 64, PressureThresholds::default());
631        let id0 = pool.acquire().expect("should acquire");
632        let id1 = pool.acquire().expect("should acquire");
633        let id2 = pool.acquire().expect("should acquire");
634        let id3 = pool.acquire().expect("should acquire");
635        assert_eq!(pool.current_pressure_level(), MemoryPressureLevel::Critical);
636        pool.release(id3);
637        assert_eq!(pool.current_pressure_level(), MemoryPressureLevel::High);
638        pool.release(id2);
639        assert_eq!(pool.current_pressure_level(), MemoryPressureLevel::Medium);
640        pool.release(id1);
641        pool.release(id0);
642        assert_eq!(pool.current_pressure_level(), MemoryPressureLevel::Low);
643    }
644
645    // --- Pressure callback tests ---
646
647    #[test]
648    fn test_pressure_callback_fired_on_transition() {
649        let events: Arc<Mutex<Vec<MemoryPressureLevel>>> = Arc::new(Mutex::new(Vec::new()));
650        let events_clone = Arc::clone(&events);
651
652        let mut pool = BufferPool::with_pressure(4, 64, PressureThresholds::default());
653        pool.add_pressure_callback(Box::new(move |level| {
654            events_clone.lock().expect("lock").push(level);
655        }));
656
657        // 2/4 = 0.5 → Medium transition
658        let _id0 = pool.acquire();
659        let _id1 = pool.acquire();
660        // 3/4 = 0.75 → High transition
661        let _id2 = pool.acquire();
662        // 4/4 = 1.0 → Critical transition
663        let _id3 = pool.acquire();
664
665        let recorded = events.lock().expect("lock").clone();
666        assert_eq!(
667            recorded,
668            vec![
669                MemoryPressureLevel::Medium,
670                MemoryPressureLevel::High,
671                MemoryPressureLevel::Critical,
672            ]
673        );
674    }
675
676    #[test]
677    fn test_pressure_callback_not_fired_on_same_level() {
678        let events: Arc<Mutex<Vec<MemoryPressureLevel>>> = Arc::new(Mutex::new(Vec::new()));
679        let events_clone = Arc::clone(&events);
680
681        // 10-buffer pool; thresholds at 0.5/0.75/0.9
682        let mut pool = BufferPool::with_pressure(10, 64, PressureThresholds::default());
683        pool.add_pressure_callback(Box::new(move |level| {
684            events_clone.lock().expect("lock").push(level);
685        }));
686
687        // acquire 2 → Low (1/10, 2/10 both < 0.5)
688        let _a = pool.acquire(); // 0.1 → Low (no transition from initial Low)
689        let _b = pool.acquire(); // 0.2 → Low (no transition)
690
691        let recorded = events.lock().expect("lock").clone();
692        // No transitions: started at Low and stayed Low
693        assert!(recorded.is_empty());
694    }
695
696    // --- Pool shrinking tests ---
697
698    #[test]
699    fn test_shrink_to_removes_free_buffers() {
700        let mut pool = BufferPool::new(8, 64);
701        let removed = pool.shrink_to(4);
702        assert_eq!(removed, 4);
703        assert_eq!(pool.total_count(), 4);
704        assert_eq!(pool.available_count(), 4);
705    }
706
707    #[test]
708    fn test_shrink_to_does_not_remove_in_use_buffers() {
709        let mut pool = BufferPool::new(4, 64);
710        let id0 = pool.acquire().expect("should acquire");
711        let id1 = pool.acquire().expect("should acquire");
712        // 2 in use, 2 free; shrink to 1 — can only remove 2 free ones, leaving 2 (the in-use)
713        let removed = pool.shrink_to(1);
714        assert_eq!(removed, 2);
715        assert_eq!(pool.total_count(), 2);
716        assert_eq!(pool.in_use_count(), 2);
717        pool.release(id0);
718        pool.release(id1);
719    }
720
721    #[test]
722    fn test_shrink_to_noop_when_already_at_or_below_target() {
723        let mut pool = BufferPool::new(4, 64);
724        let removed = pool.shrink_to(4);
725        assert_eq!(removed, 0);
726        assert_eq!(pool.total_count(), 4);
727
728        let removed2 = pool.shrink_to(10);
729        assert_eq!(removed2, 0);
730        assert_eq!(pool.total_count(), 4);
731    }
732
733    #[test]
734    fn test_auto_shrink_when_low_pressure() {
735        let mut pool = BufferPool::with_pressure(8, 64, PressureThresholds::default());
736        // All free → Low pressure; available (8) > half (4) → shrink
737        let removed = pool.auto_shrink();
738        assert!(removed > 0);
739        assert!(pool.total_count() < 8);
740    }
741
742    #[test]
743    fn test_auto_shrink_does_not_shrink_under_pressure() {
744        let mut pool = BufferPool::with_pressure(4, 64, PressureThresholds::default());
745        // Acquire 2/4 = Medium pressure
746        let _id0 = pool.acquire();
747        let _id1 = pool.acquire();
748        let removed = pool.auto_shrink();
749        assert_eq!(removed, 0);
750    }
751
752    #[test]
753    fn test_in_use_count() {
754        let mut pool = BufferPool::new(4, 64);
755        assert_eq!(pool.in_use_count(), 0);
756        let _ = pool.acquire();
757        let _ = pool.acquire();
758        assert_eq!(pool.in_use_count(), 2);
759    }
760
761    // --- Pool without pressure thresholds always reports Low ---
762
763    #[test]
764    fn test_no_thresholds_always_low() {
765        let mut pool = BufferPool::new(2, 64);
766        let _ = pool.acquire();
767        let _ = pool.acquire();
768        assert_eq!(pool.current_pressure_level(), MemoryPressureLevel::Low);
769    }
770}