Skip to main content

voirs_conversion/
buffer_pool.rs

1//! Buffer pooling for efficient memory reuse in audio processing
2//!
3//! This module provides thread-safe buffer pools to reduce allocations
4//! in hot paths like FFT operations and audio transformations.
5
6use parking_lot::Mutex;
7use scirs2_core::Complex;
8use std::sync::atomic::{AtomicU64, Ordering};
9use std::sync::Arc;
10
11/// Pool statistics for monitoring performance
12#[derive(Debug, Clone, Default)]
13pub struct PoolStatistics {
14    /// Number of buffer acquisitions from pool
15    pub hits: u64,
16    /// Number of new buffer allocations
17    pub misses: u64,
18    /// Total bytes allocated
19    pub bytes_allocated: u64,
20    /// Total bytes reused from pool
21    pub bytes_reused: u64,
22}
23
24impl PoolStatistics {
25    /// Calculate hit rate (0.0-1.0)
26    pub fn hit_rate(&self) -> f64 {
27        let total = self.hits + self.misses;
28        if total == 0 {
29            0.0
30        } else {
31            self.hits as f64 / total as f64
32        }
33    }
34
35    /// Calculate total memory efficiency
36    pub fn memory_efficiency(&self) -> f64 {
37        let total = self.bytes_allocated + self.bytes_reused;
38        if total == 0 {
39            0.0
40        } else {
41            self.bytes_reused as f64 / total as f64
42        }
43    }
44}
45
46/// Thread-local buffer pool for f32 audio samples
47pub struct AudioBufferPool {
48    /// Pool of reusable buffers
49    buffers: Arc<Mutex<Vec<Vec<f32>>>>,
50    /// Maximum number of buffers to pool
51    max_pooled: usize,
52    /// Default capacity for new buffers
53    default_capacity: usize,
54    /// Pool statistics (atomic counters)
55    hits: Arc<AtomicU64>,
56    misses: Arc<AtomicU64>,
57    bytes_allocated: Arc<AtomicU64>,
58    bytes_reused: Arc<AtomicU64>,
59}
60
61impl AudioBufferPool {
62    /// Create a new buffer pool
63    ///
64    /// # Arguments
65    /// * `max_pooled` - Maximum number of buffers to keep in pool
66    /// * `default_capacity` - Default capacity for newly allocated buffers
67    pub fn new(max_pooled: usize, default_capacity: usize) -> Self {
68        Self {
69            buffers: Arc::new(Mutex::new(Vec::with_capacity(max_pooled))),
70            max_pooled,
71            default_capacity,
72            hits: Arc::new(AtomicU64::new(0)),
73            misses: Arc::new(AtomicU64::new(0)),
74            bytes_allocated: Arc::new(AtomicU64::new(0)),
75            bytes_reused: Arc::new(AtomicU64::new(0)),
76        }
77    }
78
79    /// Get a buffer from the pool or allocate a new one
80    ///
81    /// # Arguments
82    /// * `min_capacity` - Minimum required capacity
83    ///
84    /// # Returns
85    /// A buffer with at least the requested capacity
86    pub fn acquire(&self, min_capacity: usize) -> PooledBuffer {
87        let mut buffers = self.buffers.lock();
88
89        // Try to find a buffer with sufficient capacity
90        if let Some(pos) = buffers
91            .iter()
92            .position(|buf| buf.capacity() >= min_capacity)
93        {
94            let mut buffer = buffers.swap_remove(pos);
95            let capacity = buffer.capacity();
96            buffer.clear();
97
98            // Update statistics
99            self.hits.fetch_add(1, Ordering::Relaxed);
100            self.bytes_reused.fetch_add(
101                (capacity * std::mem::size_of::<f32>()) as u64,
102                Ordering::Relaxed,
103            );
104
105            return PooledBuffer {
106                buffer: Some(buffer),
107                pool: Arc::clone(&self.buffers),
108                max_pooled: self.max_pooled,
109            };
110        }
111
112        // Allocate new buffer if none available
113        drop(buffers);
114        let capacity = min_capacity.max(self.default_capacity);
115
116        // Update statistics
117        self.misses.fetch_add(1, Ordering::Relaxed);
118        self.bytes_allocated.fetch_add(
119            (capacity * std::mem::size_of::<f32>()) as u64,
120            Ordering::Relaxed,
121        );
122
123        PooledBuffer {
124            buffer: Some(Vec::with_capacity(capacity)),
125            pool: Arc::clone(&self.buffers),
126            max_pooled: self.max_pooled,
127        }
128    }
129
130    /// Get the number of buffers currently in the pool
131    pub fn pool_size(&self) -> usize {
132        self.buffers.lock().len()
133    }
134
135    /// Clear all buffers from the pool
136    pub fn clear(&self) {
137        self.buffers.lock().clear();
138    }
139
140    /// Get pool statistics
141    pub fn statistics(&self) -> PoolStatistics {
142        PoolStatistics {
143            hits: self.hits.load(Ordering::Relaxed),
144            misses: self.misses.load(Ordering::Relaxed),
145            bytes_allocated: self.bytes_allocated.load(Ordering::Relaxed),
146            bytes_reused: self.bytes_reused.load(Ordering::Relaxed),
147        }
148    }
149
150    /// Reset pool statistics
151    pub fn reset_statistics(&self) {
152        self.hits.store(0, Ordering::Relaxed);
153        self.misses.store(0, Ordering::Relaxed);
154        self.bytes_allocated.store(0, Ordering::Relaxed);
155        self.bytes_reused.store(0, Ordering::Relaxed);
156    }
157}
158
159impl Default for AudioBufferPool {
160    fn default() -> Self {
161        Self::new(16, 4096)
162    }
163}
164
165/// A buffer borrowed from the pool that will be returned on drop
166pub struct PooledBuffer {
167    buffer: Option<Vec<f32>>,
168    pool: Arc<Mutex<Vec<Vec<f32>>>>,
169    max_pooled: usize,
170}
171
172impl PooledBuffer {
173    /// Get a mutable reference to the buffer (internal use)
174    fn get_mut(&mut self) -> &mut Vec<f32> {
175        self.buffer.as_mut().expect("Buffer already consumed")
176    }
177
178    /// Get a reference to the buffer (internal use)
179    fn get_ref(&self) -> &Vec<f32> {
180        self.buffer.as_ref().expect("Buffer already consumed")
181    }
182
183    /// Consume the pooled buffer and take ownership of the inner Vec
184    pub fn into_inner(mut self) -> Vec<f32> {
185        self.buffer.take().expect("Buffer already consumed")
186    }
187}
188
189impl Drop for PooledBuffer {
190    fn drop(&mut self) {
191        if let Some(mut buffer) = self.buffer.take() {
192            let mut pool = self.pool.lock();
193            if pool.len() < self.max_pooled {
194                buffer.clear();
195                pool.push(buffer);
196            }
197        }
198    }
199}
200
201impl std::ops::Deref for PooledBuffer {
202    type Target = Vec<f32>;
203
204    fn deref(&self) -> &Self::Target {
205        self.get_ref()
206    }
207}
208
209impl std::ops::DerefMut for PooledBuffer {
210    fn deref_mut(&mut self) -> &mut Self::Target {
211        self.get_mut()
212    }
213}
214
215/// Thread-local buffer pool for Complex numbers (FFT operations)
216pub struct ComplexBufferPool {
217    /// Pool of reusable Complex buffers
218    buffers: Arc<Mutex<Vec<Vec<Complex<f32>>>>>,
219    /// Maximum number of buffers to pool
220    max_pooled: usize,
221    /// Default capacity for new buffers
222    default_capacity: usize,
223    /// Pool statistics
224    hits: Arc<AtomicU64>,
225    misses: Arc<AtomicU64>,
226    bytes_allocated: Arc<AtomicU64>,
227    bytes_reused: Arc<AtomicU64>,
228}
229
230impl ComplexBufferPool {
231    /// Create a new Complex buffer pool
232    pub fn new(max_pooled: usize, default_capacity: usize) -> Self {
233        Self {
234            buffers: Arc::new(Mutex::new(Vec::with_capacity(max_pooled))),
235            max_pooled,
236            default_capacity,
237            hits: Arc::new(AtomicU64::new(0)),
238            misses: Arc::new(AtomicU64::new(0)),
239            bytes_allocated: Arc::new(AtomicU64::new(0)),
240            bytes_reused: Arc::new(AtomicU64::new(0)),
241        }
242    }
243
244    /// Acquire a Complex buffer from the pool
245    pub fn acquire(&self, min_capacity: usize) -> PooledComplexBuffer {
246        let mut buffers = self.buffers.lock();
247
248        // Try to find a buffer with sufficient capacity
249        if let Some(pos) = buffers
250            .iter()
251            .position(|buf| buf.capacity() >= min_capacity)
252        {
253            let mut buffer = buffers.swap_remove(pos);
254            let capacity = buffer.capacity();
255            buffer.clear();
256
257            // Update statistics
258            self.hits.fetch_add(1, Ordering::Relaxed);
259            self.bytes_reused.fetch_add(
260                (capacity * std::mem::size_of::<Complex<f32>>()) as u64,
261                Ordering::Relaxed,
262            );
263
264            return PooledComplexBuffer {
265                buffer: Some(buffer),
266                pool: Arc::clone(&self.buffers),
267                max_pooled: self.max_pooled,
268            };
269        }
270
271        // Allocate new buffer if none available
272        drop(buffers);
273        let capacity = min_capacity.max(self.default_capacity);
274
275        // Update statistics
276        self.misses.fetch_add(1, Ordering::Relaxed);
277        self.bytes_allocated.fetch_add(
278            (capacity * std::mem::size_of::<Complex<f32>>()) as u64,
279            Ordering::Relaxed,
280        );
281
282        PooledComplexBuffer {
283            buffer: Some(Vec::with_capacity(capacity)),
284            pool: Arc::clone(&self.buffers),
285            max_pooled: self.max_pooled,
286        }
287    }
288
289    /// Get pool size
290    pub fn pool_size(&self) -> usize {
291        self.buffers.lock().len()
292    }
293
294    /// Clear pool
295    pub fn clear(&self) {
296        self.buffers.lock().clear();
297    }
298
299    /// Get statistics
300    pub fn statistics(&self) -> PoolStatistics {
301        PoolStatistics {
302            hits: self.hits.load(Ordering::Relaxed),
303            misses: self.misses.load(Ordering::Relaxed),
304            bytes_allocated: self.bytes_allocated.load(Ordering::Relaxed),
305            bytes_reused: self.bytes_reused.load(Ordering::Relaxed),
306        }
307    }
308
309    /// Reset statistics
310    pub fn reset_statistics(&self) {
311        self.hits.store(0, Ordering::Relaxed);
312        self.misses.store(0, Ordering::Relaxed);
313        self.bytes_allocated.store(0, Ordering::Relaxed);
314        self.bytes_reused.store(0, Ordering::Relaxed);
315    }
316}
317
318impl Default for ComplexBufferPool {
319    fn default() -> Self {
320        Self::new(16, 4096)
321    }
322}
323
324/// A Complex buffer borrowed from the pool
325pub struct PooledComplexBuffer {
326    buffer: Option<Vec<Complex<f32>>>,
327    pool: Arc<Mutex<Vec<Vec<Complex<f32>>>>>,
328    max_pooled: usize,
329}
330
331impl PooledComplexBuffer {
332    fn get_mut(&mut self) -> &mut Vec<Complex<f32>> {
333        self.buffer.as_mut().expect("Buffer already consumed")
334    }
335
336    fn get_ref(&self) -> &Vec<Complex<f32>> {
337        self.buffer.as_ref().expect("Buffer already consumed")
338    }
339
340    /// Consume and take ownership of the buffer
341    pub fn into_inner(mut self) -> Vec<Complex<f32>> {
342        self.buffer.take().expect("Buffer already consumed")
343    }
344}
345
346impl Drop for PooledComplexBuffer {
347    fn drop(&mut self) {
348        if let Some(mut buffer) = self.buffer.take() {
349            let mut pool = self.pool.lock();
350            if pool.len() < self.max_pooled {
351                buffer.clear();
352                pool.push(buffer);
353            }
354        }
355    }
356}
357
358impl std::ops::Deref for PooledComplexBuffer {
359    type Target = Vec<Complex<f32>>;
360
361    fn deref(&self) -> &Self::Target {
362        self.get_ref()
363    }
364}
365
366impl std::ops::DerefMut for PooledComplexBuffer {
367    fn deref_mut(&mut self) -> &mut Self::Target {
368        self.get_mut()
369    }
370}
371
372// Thread-local buffer pool instances
373thread_local! {
374    static BUFFER_POOL: AudioBufferPool = AudioBufferPool::default();
375    static COMPLEX_BUFFER_POOL: ComplexBufferPool = ComplexBufferPool::default();
376}
377
378/// Get a buffer from the thread-local pool
379///
380/// # Arguments
381/// * `min_capacity` - Minimum required capacity
382///
383/// # Returns
384/// A pooled buffer that will be returned to the pool on drop
385///
386/// # Example
387/// ```
388/// use voirs_conversion::buffer_pool::get_pooled_buffer;
389///
390/// let mut buffer = get_pooled_buffer(1024);
391/// buffer.extend_from_slice(&[0.0f32; 1024]);
392/// // Buffer is automatically returned to pool when dropped
393/// ```
394pub fn get_pooled_buffer(min_capacity: usize) -> PooledBuffer {
395    BUFFER_POOL.with(|pool| pool.acquire(min_capacity))
396}
397
398/// Get a Complex buffer from the thread-local pool
399///
400/// # Arguments
401/// * `min_capacity` - Minimum required capacity
402///
403/// # Returns
404/// A pooled Complex buffer for FFT operations
405///
406/// # Example
407/// ```
408/// use voirs_conversion::buffer_pool::get_pooled_complex_buffer;
409/// use scirs2_core::Complex;
410///
411/// let mut buffer = get_pooled_complex_buffer(1024);
412/// buffer.push(Complex::new(1.0, 0.0));
413/// // Buffer is automatically returned to pool when dropped
414/// ```
415pub fn get_pooled_complex_buffer(min_capacity: usize) -> PooledComplexBuffer {
416    COMPLEX_BUFFER_POOL.with(|pool| pool.acquire(min_capacity))
417}
418
419/// Get the size of the thread-local buffer pool
420pub fn pool_size() -> usize {
421    BUFFER_POOL.with(|pool| pool.pool_size())
422}
423
424/// Get the size of the thread-local Complex buffer pool
425pub fn complex_pool_size() -> usize {
426    COMPLEX_BUFFER_POOL.with(|pool| pool.pool_size())
427}
428
429/// Clear the thread-local buffer pool
430pub fn clear_pool() {
431    BUFFER_POOL.with(|pool| pool.clear());
432}
433
434/// Clear the thread-local Complex buffer pool
435pub fn clear_complex_pool() {
436    COMPLEX_BUFFER_POOL.with(|pool| pool.clear());
437}
438
439/// Get statistics for the thread-local buffer pool
440pub fn pool_statistics() -> PoolStatistics {
441    BUFFER_POOL.with(|pool| pool.statistics())
442}
443
444/// Get statistics for the thread-local Complex buffer pool
445pub fn complex_pool_statistics() -> PoolStatistics {
446    COMPLEX_BUFFER_POOL.with(|pool| pool.statistics())
447}
448
449/// Reset statistics for the thread-local buffer pool
450pub fn reset_pool_statistics() {
451    BUFFER_POOL.with(|pool| pool.reset_statistics());
452}
453
454/// Reset statistics for the thread-local Complex buffer pool
455pub fn reset_complex_pool_statistics() {
456    COMPLEX_BUFFER_POOL.with(|pool| pool.reset_statistics());
457}
458
459#[cfg(test)]
460mod tests {
461    use super::*;
462
463    #[test]
464    fn test_buffer_pool_acquire_release() {
465        let pool = AudioBufferPool::new(4, 1024);
466
467        // Acquire buffer
468        let buffer = pool.acquire(512);
469        assert!(buffer.capacity() >= 512);
470        assert_eq!(pool.pool_size(), 0);
471
472        // Release buffer by dropping
473        drop(buffer);
474        assert_eq!(pool.pool_size(), 1);
475
476        // Reuse buffer
477        let buffer = pool.acquire(512);
478        assert_eq!(pool.pool_size(), 0);
479    }
480
481    #[test]
482    fn test_buffer_pool_capacity_growth() {
483        let pool = AudioBufferPool::new(4, 1024);
484
485        // Request larger buffer than default
486        let buffer = pool.acquire(2048);
487        assert!(buffer.capacity() >= 2048);
488    }
489
490    #[test]
491    fn test_buffer_pool_max_pooled() {
492        let pool = AudioBufferPool::new(2, 1024);
493
494        // Fill pool to maximum
495        let b1 = pool.acquire(512);
496        let b2 = pool.acquire(512);
497        let b3 = pool.acquire(512);
498
499        drop(b1);
500        drop(b2);
501        assert_eq!(pool.pool_size(), 2);
502
503        // Third buffer should not be pooled (exceeds max)
504        drop(b3);
505        assert_eq!(pool.pool_size(), 2);
506    }
507
508    #[test]
509    fn test_pooled_buffer_deref() {
510        let pool = AudioBufferPool::new(4, 1024);
511        let mut buffer = pool.acquire(512);
512
513        // Test mutable deref
514        buffer.extend_from_slice(&[1.0, 2.0, 3.0]);
515        assert_eq!(buffer.len(), 3);
516        assert_eq!(buffer[0], 1.0);
517    }
518
519    #[test]
520    fn test_pooled_buffer_into_inner() {
521        let pool = AudioBufferPool::new(4, 1024);
522        let mut buffer = pool.acquire(512);
523        buffer.extend_from_slice(&[1.0, 2.0, 3.0]);
524
525        let vec = buffer.into_inner();
526        assert_eq!(vec.len(), 3);
527        assert_eq!(vec[0], 1.0);
528
529        // Buffer was consumed, not returned to pool
530        assert_eq!(pool.pool_size(), 0);
531    }
532
533    #[test]
534    fn test_thread_local_pool() {
535        // Clear any existing buffers
536        clear_pool();
537        assert_eq!(pool_size(), 0);
538
539        // Acquire and release
540        let buffer = get_pooled_buffer(1024);
541        drop(buffer);
542        assert_eq!(pool_size(), 1);
543
544        // Reuse
545        let buffer = get_pooled_buffer(512);
546        assert_eq!(pool_size(), 0);
547        drop(buffer);
548        assert_eq!(pool_size(), 1);
549    }
550
551    #[test]
552    fn test_clear_pool() {
553        clear_pool();
554        let b1 = get_pooled_buffer(512);
555        let b2 = get_pooled_buffer(512);
556        drop(b1);
557        drop(b2);
558        assert_eq!(pool_size(), 2);
559
560        clear_pool();
561        assert_eq!(pool_size(), 0);
562    }
563
564    #[test]
565    fn test_pool_statistics() {
566        let pool = AudioBufferPool::new(4, 1024);
567        pool.reset_statistics();
568
569        // First acquisition - should be a miss
570        let b1 = pool.acquire(512);
571        drop(b1);
572
573        let stats = pool.statistics();
574        assert_eq!(stats.misses, 1);
575        assert_eq!(stats.hits, 0);
576        assert!(stats.bytes_allocated > 0);
577
578        // Second acquisition - should be a hit
579        let b2 = pool.acquire(512);
580        drop(b2);
581
582        let stats = pool.statistics();
583        assert_eq!(stats.misses, 1);
584        assert_eq!(stats.hits, 1);
585        assert!(stats.bytes_reused > 0);
586
587        // Check hit rate
588        assert!((stats.hit_rate() - 0.5).abs() < 0.01);
589    }
590
591    #[test]
592    fn test_pool_statistics_memory_efficiency() {
593        let pool = AudioBufferPool::new(4, 1024);
594        pool.reset_statistics();
595
596        // Allocate and reuse multiple times
597        for _ in 0..10 {
598            let b = pool.acquire(512);
599            drop(b);
600        }
601
602        let stats = pool.statistics();
603        // First allocation is a miss, rest are hits
604        assert_eq!(stats.misses, 1);
605        assert_eq!(stats.hits, 9);
606
607        // Memory efficiency should be high (90%)
608        assert!(stats.memory_efficiency() > 0.85);
609    }
610
611    #[test]
612    fn test_complex_buffer_pool() {
613        let pool = ComplexBufferPool::new(4, 1024);
614
615        // Acquire complex buffer
616        let mut buffer = pool.acquire(512);
617        assert!(buffer.capacity() >= 512);
618
619        // Use the buffer
620        buffer.push(Complex::new(1.0, 2.0));
621        buffer.push(Complex::new(3.0, 4.0));
622        assert_eq!(buffer.len(), 2);
623
624        // Release and verify pooling
625        drop(buffer);
626        assert_eq!(pool.pool_size(), 1);
627
628        // Reuse buffer
629        let buffer = pool.acquire(512);
630        assert_eq!(buffer.len(), 0); // Should be cleared
631        assert_eq!(pool.pool_size(), 0);
632    }
633
634    #[test]
635    fn test_complex_buffer_statistics() {
636        let pool = ComplexBufferPool::new(4, 1024);
637        pool.reset_statistics();
638
639        // First acquisition
640        let b1 = pool.acquire(512);
641        drop(b1);
642
643        let stats = pool.statistics();
644        assert_eq!(stats.misses, 1);
645        assert_eq!(stats.hits, 0);
646
647        // Verify bytes allocated for Complex<f32>
648        let expected_bytes = 1024 * std::mem::size_of::<Complex<f32>>();
649        assert_eq!(stats.bytes_allocated as usize, expected_bytes);
650
651        // Second acquisition - hit
652        let b2 = pool.acquire(512);
653        drop(b2);
654
655        let stats = pool.statistics();
656        assert_eq!(stats.hits, 1);
657        assert!(stats.bytes_reused > 0);
658    }
659
660    #[test]
661    fn test_thread_local_complex_pool() {
662        clear_complex_pool();
663        reset_complex_pool_statistics();
664
665        // Acquire and release
666        let mut buffer = get_pooled_complex_buffer(1024);
667        buffer.push(Complex::new(1.0, 0.0));
668        drop(buffer);
669
670        assert_eq!(complex_pool_size(), 1);
671
672        // Check statistics
673        let stats = complex_pool_statistics();
674        assert_eq!(stats.misses, 1);
675        assert_eq!(stats.hits, 0);
676
677        // Reuse
678        let buffer = get_pooled_complex_buffer(512);
679        drop(buffer);
680
681        let stats = complex_pool_statistics();
682        assert_eq!(stats.hits, 1);
683        assert!(stats.hit_rate() > 0.4);
684    }
685
686    #[test]
687    fn test_pooled_complex_buffer_deref() {
688        let pool = ComplexBufferPool::new(4, 1024);
689        let mut buffer = pool.acquire(512);
690
691        // Test mutable deref
692        buffer.extend_from_slice(&[Complex::new(1.0, 2.0), Complex::new(3.0, 4.0)]);
693
694        assert_eq!(buffer.len(), 2);
695        assert_eq!(buffer[0], Complex::new(1.0, 2.0));
696        assert_eq!(buffer[1], Complex::new(3.0, 4.0));
697    }
698
699    #[test]
700    fn test_pooled_complex_buffer_into_inner() {
701        let pool = ComplexBufferPool::new(4, 1024);
702        let mut buffer = pool.acquire(512);
703
704        buffer.push(Complex::new(1.0, 2.0));
705        let vec = buffer.into_inner();
706
707        assert_eq!(vec.len(), 1);
708        assert_eq!(vec[0], Complex::new(1.0, 2.0));
709
710        // Buffer was consumed, not returned to pool
711        assert_eq!(pool.pool_size(), 0);
712    }
713
714    #[test]
715    fn test_statistics_reset() {
716        let pool = AudioBufferPool::new(4, 1024);
717
718        // Generate some activity
719        for _ in 0..5 {
720            let b = pool.acquire(512);
721            drop(b);
722        }
723
724        let stats = pool.statistics();
725        assert!(stats.hits > 0 || stats.misses > 0);
726
727        // Reset statistics
728        pool.reset_statistics();
729        let stats = pool.statistics();
730        assert_eq!(stats.hits, 0);
731        assert_eq!(stats.misses, 0);
732        assert_eq!(stats.bytes_allocated, 0);
733        assert_eq!(stats.bytes_reused, 0);
734    }
735
736    #[test]
737    fn test_thread_local_statistics() {
738        reset_pool_statistics();
739        reset_complex_pool_statistics();
740
741        // Regular buffer stats
742        let b1 = get_pooled_buffer(1024);
743        drop(b1);
744
745        let stats = pool_statistics();
746        assert_eq!(stats.misses, 1);
747
748        // Complex buffer stats
749        let b2 = get_pooled_complex_buffer(1024);
750        drop(b2);
751
752        let complex_stats = complex_pool_statistics();
753        assert_eq!(complex_stats.misses, 1);
754
755        // Verify they're independent
756        assert_eq!(stats.misses, 1);
757        assert_eq!(complex_stats.misses, 1);
758    }
759}