Skip to main content

ipfrs_core/
pool.rs

1//! Memory pooling for frequent allocations
2//!
3//! This module provides memory pools for common allocation patterns:
4//! - Block buffer pool (reuse Bytes allocations)
5//! - CID string pool (deduplicate strings)
6//! - IPLD node pool
7//!
8//! Memory pooling reduces allocator pressure by reusing existing allocations.
9
10use bytes::{Bytes, BytesMut};
11use std::collections::HashMap;
12use std::sync::{Arc, Mutex};
13
14/// A pool of reusable byte buffers
15///
16/// This pool maintains a collection of BytesMut buffers that can be reused
17/// to reduce allocation overhead when creating blocks.
18#[derive(Clone)]
19pub struct BytesPool {
20    /// Available buffers, organized by capacity bucket
21    /// Each bucket contains buffers with capacity in a power-of-2 range
22    pool: Arc<Mutex<HashMap<usize, Vec<BytesMut>>>>,
23    /// Statistics for the pool
24    stats: Arc<Mutex<PoolStats>>,
25}
26
27impl Default for BytesPool {
28    fn default() -> Self {
29        Self::new()
30    }
31}
32
33impl BytesPool {
34    /// Create a new bytes pool
35    pub fn new() -> Self {
36        Self {
37            pool: Arc::new(Mutex::new(HashMap::new())),
38            stats: Arc::new(Mutex::new(PoolStats::default())),
39        }
40    }
41
42    /// Get a buffer with at least the requested capacity
43    ///
44    /// If a suitable buffer is available in the pool, it will be reused.
45    /// Otherwise, a new buffer will be allocated.
46    pub fn get(&self, capacity: usize) -> BytesMut {
47        let bucket = Self::capacity_bucket(capacity);
48
49        let mut pool = self.pool.lock().unwrap_or_else(|e| e.into_inner());
50        let mut stats = self.stats.lock().unwrap_or_else(|e| e.into_inner());
51
52        if let Some(buffers) = pool.get_mut(&bucket) {
53            if let Some(mut buf) = buffers.pop() {
54                buf.clear();
55                stats.hits += 1;
56                return buf;
57            }
58        }
59
60        stats.misses += 1;
61        stats.allocations += 1;
62        BytesMut::with_capacity(bucket)
63    }
64
65    /// Return a buffer to the pool for reuse
66    ///
67    /// The buffer will be cleared and made available for future use.
68    pub fn put(&self, mut buf: BytesMut) {
69        // Only pool buffers within reasonable size limits
70        if buf.capacity() > 1024 * 1024 * 4 {
71            // Too large, don't pool
72            return;
73        }
74
75        buf.clear();
76        let bucket = Self::capacity_bucket(buf.capacity());
77
78        let mut pool = self.pool.lock().unwrap_or_else(|e| e.into_inner());
79        let buffers = pool.entry(bucket).or_default();
80
81        // Limit pool size per bucket to prevent unbounded growth
82        if buffers.len() < 100 {
83            buffers.push(buf);
84        }
85    }
86
87    /// Get the pool statistics
88    pub fn stats(&self) -> PoolStats {
89        *self.stats.lock().unwrap_or_else(|e| e.into_inner())
90    }
91
92    /// Clear all pooled buffers
93    pub fn clear(&self) {
94        self.pool.lock().unwrap_or_else(|e| e.into_inner()).clear();
95    }
96
97    /// Round capacity up to the nearest power-of-2 bucket
98    fn capacity_bucket(capacity: usize) -> usize {
99        if capacity == 0 {
100            return 1024; // Minimum 1KB
101        }
102        capacity.next_power_of_two().max(1024)
103    }
104}
105
106/// A pool for CID strings to reduce duplication
107///
108/// This pool maintains a cache of CID strings that have been seen before,
109/// allowing them to be deduplicated and reused.
110#[derive(Clone)]
111pub struct CidStringPool {
112    /// Interned strings
113    pool: Arc<Mutex<HashMap<String, Arc<str>>>>,
114    /// Statistics for the pool
115    stats: Arc<Mutex<PoolStats>>,
116}
117
118impl Default for CidStringPool {
119    fn default() -> Self {
120        Self::new()
121    }
122}
123
124impl CidStringPool {
125    /// Create a new CID string pool
126    pub fn new() -> Self {
127        Self {
128            pool: Arc::new(Mutex::new(HashMap::new())),
129            stats: Arc::new(Mutex::new(PoolStats::default())),
130        }
131    }
132
133    /// Intern a CID string
134    ///
135    /// If the string has been seen before, returns the existing Arc.
136    /// Otherwise, creates a new Arc and stores it in the pool.
137    pub fn intern(&self, s: &str) -> Arc<str> {
138        let mut pool = self.pool.lock().unwrap_or_else(|e| e.into_inner());
139        let mut stats = self.stats.lock().unwrap_or_else(|e| e.into_inner());
140
141        if let Some(existing) = pool.get(s) {
142            stats.hits += 1;
143            return Arc::clone(existing);
144        }
145
146        stats.misses += 1;
147        let arc: Arc<str> = Arc::from(s);
148        pool.insert(s.to_string(), Arc::clone(&arc));
149        arc
150    }
151
152    /// Get the pool statistics
153    pub fn stats(&self) -> PoolStats {
154        *self.stats.lock().unwrap_or_else(|e| e.into_inner())
155    }
156
157    /// Clear the pool
158    pub fn clear(&self) {
159        self.pool.lock().unwrap_or_else(|e| e.into_inner()).clear();
160    }
161
162    /// Get the number of unique strings in the pool
163    pub fn len(&self) -> usize {
164        self.pool.lock().unwrap_or_else(|e| e.into_inner()).len()
165    }
166
167    /// Check if the pool is empty
168    pub fn is_empty(&self) -> bool {
169        self.pool
170            .lock()
171            .unwrap_or_else(|e| e.into_inner())
172            .is_empty()
173    }
174}
175
176/// Statistics for a memory pool
177#[derive(Debug, Clone, Copy, Default)]
178pub struct PoolStats {
179    /// Number of successful retrievals from the pool
180    pub hits: u64,
181    /// Number of times a new allocation was needed
182    pub misses: u64,
183    /// Total number of allocations made
184    pub allocations: u64,
185}
186
187impl PoolStats {
188    /// Calculate the hit rate (0.0 to 1.0)
189    pub fn hit_rate(&self) -> f64 {
190        let total = self.hits + self.misses;
191        if total == 0 {
192            return 0.0;
193        }
194        self.hits as f64 / total as f64
195    }
196
197    /// Calculate the miss rate (0.0 to 1.0)
198    pub fn miss_rate(&self) -> f64 {
199        1.0 - self.hit_rate()
200    }
201}
202
203/// A global bytes pool instance
204static GLOBAL_BYTES_POOL: once_cell::sync::Lazy<BytesPool> =
205    once_cell::sync::Lazy::new(BytesPool::new);
206
207/// A global CID string pool instance
208static GLOBAL_CID_STRING_POOL: once_cell::sync::Lazy<CidStringPool> =
209    once_cell::sync::Lazy::new(CidStringPool::new);
210
211/// Get the global bytes pool
212pub fn global_bytes_pool() -> &'static BytesPool {
213    &GLOBAL_BYTES_POOL
214}
215
216/// Get the global CID string pool
217pub fn global_cid_string_pool() -> &'static CidStringPool {
218    &GLOBAL_CID_STRING_POOL
219}
220
221/// Helper to convert BytesMut to Bytes efficiently
222pub fn freeze_bytes(buf: BytesMut) -> Bytes {
223    buf.freeze()
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229
230    #[test]
231    fn test_bytes_pool_basic() {
232        let pool = BytesPool::new();
233
234        // Get a buffer
235        let buf1 = pool.get(1024);
236        assert!(buf1.capacity() >= 1024);
237
238        // Return it
239        pool.put(buf1);
240
241        // Get another buffer - should reuse the same one
242        let buf2 = pool.get(1024);
243        assert!(buf2.capacity() >= 1024);
244
245        let stats = pool.stats();
246        assert_eq!(stats.hits, 1);
247        assert_eq!(stats.misses, 1);
248    }
249
250    #[test]
251    fn test_bytes_pool_capacity_bucketing() {
252        let pool = BytesPool::new();
253
254        // Request different sizes
255        let buf1 = pool.get(100);
256        let buf2 = pool.get(1000);
257        let buf3 = pool.get(2000);
258
259        // All should be bucketed to power-of-2 sizes
260        assert!(buf1.capacity() >= 100);
261        assert!(buf2.capacity() >= 1000);
262        assert!(buf3.capacity() >= 2000);
263
264        pool.put(buf1);
265        pool.put(buf2);
266        pool.put(buf3);
267
268        // Request similar sizes - should reuse
269        let buf4 = pool.get(150); // Should reuse first bucket
270        let buf5 = pool.get(1100); // Should reuse second bucket
271
272        assert!(buf4.capacity() >= 150);
273        assert!(buf5.capacity() >= 1100);
274    }
275
276    #[test]
277    fn test_cid_string_pool_basic() {
278        let pool = CidStringPool::new();
279
280        // Intern a string
281        let s1 = pool.intern("QmTest123");
282        let s2 = pool.intern("QmTest123");
283
284        // Should be the same Arc
285        assert_eq!(s1.as_ref(), s2.as_ref());
286        assert!(Arc::ptr_eq(&s1, &s2));
287
288        let stats = pool.stats();
289        assert_eq!(stats.hits, 1);
290        assert_eq!(stats.misses, 1);
291    }
292
293    #[test]
294    fn test_cid_string_pool_different_strings() {
295        let pool = CidStringPool::new();
296
297        let s1 = pool.intern("QmTest1");
298        let s2 = pool.intern("QmTest2");
299
300        // Should be different
301        assert_ne!(s1.as_ref(), s2.as_ref());
302        assert!(!Arc::ptr_eq(&s1, &s2));
303
304        let stats = pool.stats();
305        assert_eq!(stats.hits, 0);
306        assert_eq!(stats.misses, 2);
307    }
308
309    #[test]
310    fn test_pool_stats_hit_rate() {
311        let stats = PoolStats {
312            hits: 80,
313            misses: 20,
314            allocations: 20,
315        };
316
317        assert!((stats.hit_rate() - 0.8).abs() < 0.001);
318        assert!((stats.miss_rate() - 0.2).abs() < 0.001);
319    }
320
321    #[test]
322    fn test_pool_stats_empty() {
323        let stats = PoolStats::default();
324        assert_eq!(stats.hit_rate(), 0.0);
325        assert_eq!(stats.miss_rate(), 1.0);
326    }
327
328    #[test]
329    fn test_bytes_pool_clear() {
330        let pool = BytesPool::new();
331        let buf = pool.get(1024);
332        pool.put(buf);
333
334        pool.clear();
335
336        // After clear, should allocate new buffer
337        let _buf2 = pool.get(1024);
338        let stats = pool.stats();
339        assert_eq!(stats.misses, 2); // Both allocations were misses
340    }
341
342    #[test]
343    fn test_cid_string_pool_len() {
344        let pool = CidStringPool::new();
345        assert_eq!(pool.len(), 0);
346        assert!(pool.is_empty());
347
348        pool.intern("QmTest1");
349        assert_eq!(pool.len(), 1);
350        assert!(!pool.is_empty());
351
352        pool.intern("QmTest2");
353        assert_eq!(pool.len(), 2);
354
355        pool.intern("QmTest1"); // Duplicate
356        assert_eq!(pool.len(), 2); // Should still be 2
357    }
358
359    #[test]
360    fn test_bytes_pool_size_limit() {
361        let pool = BytesPool::new();
362
363        // Very large buffer shouldn't be pooled
364        let large_buf = BytesMut::with_capacity(10 * 1024 * 1024);
365        pool.put(large_buf);
366
367        // Try to get a large buffer - should allocate new
368        let _buf = pool.get(10 * 1024 * 1024);
369        let stats = pool.stats();
370
371        // Both should be misses (large buffer wasn't pooled)
372        assert!(stats.misses >= 1);
373    }
374
375    #[test]
376    fn test_global_pools() {
377        let bytes_pool = global_bytes_pool();
378        let cid_pool = global_cid_string_pool();
379
380        // Just ensure they're accessible
381        let _buf = bytes_pool.get(1024);
382        let _s = cid_pool.intern("QmTest");
383    }
384
385    #[test]
386    fn test_freeze_bytes() {
387        let mut buf = BytesMut::with_capacity(1024);
388        buf.extend_from_slice(b"Hello, world!");
389        let bytes = freeze_bytes(buf);
390        assert_eq!(&bytes[..], b"Hello, world!");
391    }
392}