Skip to main content

torsh_tensor/
memory_pool.rs

1// Framework infrastructure - components designed for future use
2#![allow(dead_code)]
3// Memory pooling for efficient tensor memory management with SciRS2 Memory Optimization
4
5use crate::{Tensor, TensorStorage};
6use std::alloc::{handle_alloc_error, Layout};
7use std::collections::{HashMap, VecDeque};
8use std::marker::PhantomData;
9use std::mem::{ManuallyDrop, MaybeUninit};
10use std::ptr::NonNull;
11use std::sync::{Arc, Mutex, Weak};
12use torsh_core::{device::DeviceType, dtype::TensorElement, error::Result};
13
14// ✅ SciRS2 Memory Optimization Features
15use scirs2_core::memory::GlobalBufferPool;
16use scirs2_core::memory::LeakDetector;
17// ✅ SciRS2 memory_efficient features — the real disk-backed memory-mapped array.
18// Enabled through the `memory_efficient` feature (which turns on `scirs2-core/memory_efficient`).
19#[cfg(feature = "memory_efficient")]
20use scirs2_core::memory_efficient::{AccessMode, MemoryMappedArray};
21
22/// Build a unique backing-file path for a memory-mapped allocation under the system
23/// temporary directory ([`std::env::temp_dir`]).
24#[cfg(feature = "memory_efficient")]
25fn unique_mmap_path(tag: &str) -> std::path::PathBuf {
26    use std::sync::atomic::{AtomicU64, Ordering};
27    static COUNTER: AtomicU64 = AtomicU64::new(0);
28    let seq = COUNTER.fetch_add(1, Ordering::Relaxed);
29    let nanos = std::time::SystemTime::now()
30        .duration_since(std::time::UNIX_EPOCH)
31        .unwrap_or_default()
32        .as_nanos();
33    std::env::temp_dir().join(format!(
34        "torsh_mmap_{tag}_{pid}_{nanos}_{seq}.bin",
35        pid = std::process::id()
36    ))
37}
38
39/// Round-trip `data` through a disk-backed [`MemoryMappedArray`] and return the mapped contents.
40///
41/// The data is written to `backing_path` via a memory map in [`AccessMode::Write`] and then read
42/// back through the map's [`MemoryMappedArray::as_slice`], so the returned `Vec` genuinely
43/// originates from the memory-mapped region rather than the in-memory input. The staging file is
44/// removed afterwards (best effort) because the materialised tensor no longer depends on it.
45#[cfg(feature = "memory_efficient")]
46fn map_through_mmap_file<T: TensorElement>(
47    data: Vec<T>,
48    backing_path: &std::path::Path,
49) -> Result<Vec<T>> {
50    use scirs2_core::ndarray::Array1;
51
52    // Persist the data to the memory-mapped file.
53    let array: Array1<T> = Array1::from(data);
54    let mmap = MemoryMappedArray::<T>::new(Some(&array), backing_path, AccessMode::Write, 0)
55        .map_err(|e| {
56            torsh_core::error::TorshError::IoError(format!(
57                "memory-mapped allocation failed at {path}: {e}",
58                path = backing_path.display()
59            ))
60        })?;
61
62    // Materialise the data from the memory-mapped region via `as_slice()`.
63    let mapped = mmap.as_slice().to_vec();
64
65    // Release the mapping before removing the staging file (required on some platforms).
66    drop(mmap);
67    let _ = std::fs::remove_file(backing_path);
68
69    Ok(mapped)
70}
71
72// TODO: profile_section macro not available in scirs2_core yet
73// #[cfg(feature = "profiling")]
74// use scirs2_core::profiling::profile_section;
75
76/// Global memory pool for tensor allocations
77static MEMORY_POOL: std::sync::OnceLock<Arc<Mutex<GlobalMemoryPool>>> = std::sync::OnceLock::new();
78
79/// Initialize the global memory pool
80pub fn init_memory_pool() -> Arc<Mutex<GlobalMemoryPool>> {
81    let arc = MEMORY_POOL
82        .get_or_init(|| {
83            let pool = Arc::new(Mutex::new(GlobalMemoryPool::new()));
84            // Store the Weak reference back into the pool so acquire_uninit can use it
85            if let Ok(mut guard) = pool.lock() {
86                guard.self_weak = Some(Arc::downgrade(&pool));
87            }
88            pool
89        })
90        .clone();
91    arc
92}
93
94/// Get reference to the global memory pool
95pub fn get_memory_pool() -> Arc<Mutex<GlobalMemoryPool>> {
96    init_memory_pool()
97}
98
99// ─── RawEntry ────────────────────────────────────────────────────────────────
100
101/// An owned raw allocation stored in the pool's free-list.
102/// On `Drop` it deallocates the memory if it was not consumed.
103struct RawEntry {
104    ptr: NonNull<u8>,
105    capacity_bytes: usize,
106    layout: Layout,
107}
108
109/// SAFETY: `RawEntry` owns the raw pointer; transferring it to another thread is safe.
110unsafe impl Send for RawEntry {}
111
112impl Drop for RawEntry {
113    fn drop(&mut self) {
114        // SAFETY: ptr was allocated with this layout via `std::alloc::alloc`.
115        unsafe { std::alloc::dealloc(self.ptr.as_ptr(), self.layout) };
116    }
117}
118
119// ─── ReusedBuffer<T> ─────────────────────────────────────────────────────────
120
121/// A truly-pooled buffer: holds the **actual pooled allocation** without copying.
122///
123/// When dropped (or via `release_to_pool`), the buffer is returned to the global
124/// pool. Use `into_vec(len)` to take ownership as a `Vec<T>`.
125pub struct ReusedBuffer<T: 'static> {
126    ptr: NonNull<T>,
127    capacity: usize,
128    layout: Layout,
129    pool: Weak<Mutex<GlobalMemoryPool>>,
130}
131
132/// SAFETY: `ReusedBuffer<T>` owns a unique allocation; it is safe to send across threads
133/// when `T: Send`.
134unsafe impl<T: Send + 'static> Send for ReusedBuffer<T> {}
135
136impl<T: 'static> ReusedBuffer<T> {
137    /// Returns a mutable view of the buffer as uninitialized elements.
138    pub fn as_uninit_slice_mut(&mut self) -> &mut [MaybeUninit<T>] {
139        // SAFETY: ptr is valid for `capacity` elements; we have exclusive access via &mut self.
140        unsafe {
141            std::slice::from_raw_parts_mut(self.ptr.as_ptr() as *mut MaybeUninit<T>, self.capacity)
142        }
143    }
144
145    /// Capacity in elements (not bytes).
146    pub fn capacity(&self) -> usize {
147        self.capacity
148    }
149
150    /// Raw pointer access — primarily for tests to verify address identity.
151    pub fn as_ptr_raw(&self) -> *mut T {
152        self.ptr.as_ptr()
153    }
154
155    /// Consume `self` and transfer ownership of the allocation to a `Vec<T>`.
156    ///
157    /// The caller must guarantee `len <= self.capacity()` and that the first `len`
158    /// elements have been initialized.
159    ///
160    /// The `Vec` now owns the memory and will free it on drop; it is NOT returned
161    /// to the pool.
162    ///
163    /// # Custom alignment
164    /// A `Vec<T>` always deallocates with `Layout::array::<T>()`, i.e. alignment
165    /// `align_of::<T>()`. A buffer acquired through
166    /// [`GlobalMemoryPool::acquire_uninit_aligned`] with a larger alignment
167    /// therefore cannot hand its allocation to a `Vec` — that would be a
168    /// mismatched-`Layout` deallocation (undefined behaviour). Such buffers are
169    /// copied into a fresh `Vec` instead and the over-aligned allocation is
170    /// returned to the pool, where it can still be reused.
171    pub fn into_vec(self, len: usize) -> Vec<T>
172    where
173        T: Copy,
174    {
175        debug_assert!(len <= self.capacity, "len must not exceed capacity");
176
177        if self.layout.align() != std::mem::align_of::<T>() {
178            // SAFETY: the caller guarantees the first `len` elements are
179            // initialized and `len <= capacity`.
180            let initialized =
181                unsafe { std::slice::from_raw_parts(self.ptr.as_ptr() as *const T, len) };
182            let copy = initialized.to_vec();
183            // `self` drops here → the over-aligned allocation goes back to the
184            // pool with its original layout intact.
185            return copy;
186        }
187
188        // Wrap self in ManuallyDrop so our Drop impl does not run.
189        let md = ManuallyDrop::new(self);
190        // SAFETY: ptr was allocated with the global allocator for `md.capacity` elements
191        // with `Layout::array::<T>()`-compatible alignment (checked above).
192        // `len` elements are initialized (caller contract). capacity matches.
193        unsafe { Vec::from_raw_parts(md.ptr.as_ptr(), len, md.capacity) }
194    }
195
196    /// Consume `self` and return the buffer to the pool.
197    ///
198    /// If the pool is gone (Arc was dropped), the allocation is freed instead.
199    pub fn release_to_pool(self) {
200        // Wrap in ManuallyDrop to prevent our Drop from running.
201        let md = ManuallyDrop::new(self);
202        let raw_entry = RawEntry {
203            ptr: NonNull::new(md.ptr.as_ptr() as *mut u8)
204                .expect("ReusedBuffer pointer is non-null by construction"),
205            capacity_bytes: md.capacity * std::mem::size_of::<T>(),
206            layout: md.layout,
207        };
208        if let Some(pool_arc) = md.pool.upgrade() {
209            // Recover from poisoning rather than treat it as fatal: a poisoned
210            // pool's inner state is still structurally valid (the only known
211            // panic-while-held path validates arguments before mutating pool
212            // state), so `release_to_pool` should keep pooling buffers instead
213            // of silently degrading to "always deallocate" forever after one
214            // poisoning event.
215            let mut guard = pool_arc
216                .lock()
217                .unwrap_or_else(|poisoned| poisoned.into_inner());
218            let type_id = std::any::TypeId::of::<T>();
219            let size_class = guard.find_size_class(raw_entry.capacity_bytes);
220            let align = raw_entry.layout.align();
221            let pool_key = (type_id, size_class, align);
222            if let Some(bucket) = guard.pools.get_mut(&pool_key) {
223                if bucket.available_buffers.len() < bucket.max_buffers {
224                    bucket.available_buffers.push_back(raw_entry);
225                    bucket.deallocations += 1;
226                    // ManuallyDrop prevents double-free: raw_entry is now owned by the bucket.
227                    return;
228                }
229            }
230        }
231        // Pool unavailable or full — `raw_entry` drops here and frees memory via RawEntry::Drop.
232    }
233}
234
235impl<T: 'static> Drop for ReusedBuffer<T> {
236    fn drop(&mut self) {
237        // Reconstruct a RawEntry to trigger a properly-guarded dealloc-or-return.
238        // We cannot call release_to_pool(self) directly (consumes), so replicate logic.
239        let raw_entry = RawEntry {
240            ptr: NonNull::new(self.ptr.as_ptr() as *mut u8)
241                .expect("ReusedBuffer pointer is non-null by construction"),
242            capacity_bytes: self.capacity * std::mem::size_of::<T>(),
243            layout: self.layout,
244        };
245        if let Some(pool_arc) = self.pool.upgrade() {
246            // Recover from poisoning rather than treat it as fatal (see the
247            // matching comment in `ReusedBuffer::release_to_pool`).
248            let mut guard = pool_arc
249                .lock()
250                .unwrap_or_else(|poisoned| poisoned.into_inner());
251            let type_id = std::any::TypeId::of::<T>();
252            let size_class = guard.find_size_class(raw_entry.capacity_bytes);
253            let align = raw_entry.layout.align();
254            let pool_key = (type_id, size_class, align);
255            if let Some(bucket) = guard.pools.get_mut(&pool_key) {
256                if bucket.available_buffers.len() < bucket.max_buffers {
257                    // Wrap in ManuallyDrop so push_back takes it without scheduling
258                    // a double-free when the local binding goes out of scope.
259                    let md_entry = ManuallyDrop::new(raw_entry);
260                    // SAFETY: ManuallyDrop<RawEntry> has the same layout as RawEntry;
261                    // we read it once here and never again.
262                    bucket
263                        .available_buffers
264                        .push_back(unsafe { std::ptr::read(&*md_entry as *const RawEntry) });
265                    bucket.deallocations += 1;
266                    return;
267                }
268            }
269        }
270        // raw_entry drops here → dealloc via RawEntry::Drop
271    }
272}
273
274// ─── GlobalMemoryPool ────────────────────────────────────────────────────────
275
276/// Enhanced global memory pool with SciRS2 memory optimization
277pub struct GlobalMemoryPool {
278    /// Pools organized by (type ID, size class, alignment).
279    ///
280    /// Alignment is included in the bucket key so that callers requesting custom
281    /// alignment (e.g. 32-byte SIMD alignment) do not collide with naturally-aligned
282    /// allocations of the same type+size.
283    pools: HashMap<(std::any::TypeId, usize, usize), MemoryPool>,
284    /// Statistics for pool usage
285    stats: PoolStatistics,
286    /// Configuration settings
287    config: PoolConfig,
288    /// ✅ SciRS2 Global Buffer Pool integration
289    scirs2_pool: GlobalBufferPool,
290    /// ✅ SciRS2 Memory leak detector.
291    ///
292    /// Purely a diagnostic aid: if it fails to initialize the pool degrades to
293    /// running without leak detection instead of making tensor allocation
294    /// impossible.
295    leak_detector: Option<LeakDetector>,
296    /// Weak self-reference used to hand out pool handles to `ReusedBuffer`.
297    self_weak: Option<Weak<Mutex<GlobalMemoryPool>>>,
298    // ✅ SciRS2 Memory metrics collector (requires memory_efficient feature)
299    // metrics_collector: MemoryMetricsCollector,
300    // ✅ SciRS2 Adaptive chunking for large tensors (requires memory_efficient feature)
301    // adaptive_chunking: AdaptiveChunking,
302}
303
304/// Memory pool for specific data type and size class
305#[derive(Debug)]
306struct MemoryPool {
307    /// Available buffers ready for reuse (raw allocations)
308    available_buffers: VecDeque<RawEntry>,
309    /// Size class this pool manages (in bytes)
310    #[allow(dead_code)]
311    size_class: usize,
312    /// Maximum number of buffers to keep
313    max_buffers: usize,
314    /// Statistics for this pool
315    allocations: usize,
316    reuses: usize,
317    deallocations: usize,
318}
319
320/// Configuration for memory pool behavior
321#[derive(Debug, Clone)]
322pub struct PoolConfig {
323    /// Maximum number of buffers per size class
324    pub max_buffers_per_class: usize,
325    /// Maximum total memory to use for pooling (in bytes)
326    pub max_total_memory: usize,
327    /// Enable automatic pool cleanup
328    pub auto_cleanup: bool,
329    /// Cleanup threshold (trigger cleanup when usage exceeds this ratio)
330    pub cleanup_threshold: f64,
331    /// Size classes (in bytes) - powers of 2 for efficient alignment
332    pub size_classes: Vec<usize>,
333}
334
335/// Statistics for memory pool usage
336#[derive(Debug, Default, Clone)]
337pub struct PoolStatistics {
338    /// Total number of allocations served
339    pub total_allocations: usize,
340    /// Number of allocations served from pool (reused)
341    pub pool_hits: usize,
342    /// Number of allocations that required new memory
343    pub pool_misses: usize,
344    /// Total bytes allocated
345    pub total_bytes_allocated: usize,
346    /// Total bytes currently in pools
347    pub bytes_in_pools: usize,
348    /// Peak memory usage
349    pub peak_memory_usage: usize,
350}
351
352/// A pooled tensor that automatically returns memory to pool when dropped
353#[derive(Debug)]
354pub struct PooledTensor<T: TensorElement + Default> {
355    tensor: Tensor<T>,
356    pool_key: Option<(std::any::TypeId, usize, usize)>,
357    _phantom: PhantomData<T>,
358}
359
360impl Default for PoolConfig {
361    fn default() -> Self {
362        // Generate size classes as powers of 2 from 1KB to 1GB
363        let size_classes = (10..31) // 2^10 to 2^30 bytes (1KB to 1GB)
364            .map(|exp| 1 << exp)
365            .collect();
366
367        Self {
368            max_buffers_per_class: 16,
369            max_total_memory: 1024 * 1024 * 1024, // 1GB
370            auto_cleanup: true,
371            cleanup_threshold: 0.8,
372            size_classes,
373        }
374    }
375}
376
377impl Default for GlobalMemoryPool {
378    fn default() -> Self {
379        Self::new()
380    }
381}
382
383/// Validate that `align` is usable for `T`: it must be a power of two and at
384/// least `align_of::<T>()`. Panics otherwise (see callers' `# Panics` docs).
385///
386/// This is a standalone, pre-lock-safe check shared by
387/// [`GlobalMemoryPool::acquire_uninit_aligned`] and [`global_acquire_uninit_aligned`]
388/// specifically so that [`global_acquire_uninit_aligned`] can validate `align`
389/// *before* acquiring the global `MEMORY_POOL` mutex. A `Mutex` poisons on *any*
390/// panicking unwind while it is held -- including an intentional one from a
391/// `#[should_panic]` test -- so validating first means a caller error here
392/// (which depends only on `align` and `T`, never on pool state) can never poison
393/// the global pool lock for every other thread/test in the process.
394fn assert_valid_alignment<T>(align: usize) {
395    let element_align = std::mem::align_of::<T>();
396    assert!(
397        align.is_power_of_two(),
398        "alignment must be a power of two (got {align})"
399    );
400    assert!(
401        align >= element_align,
402        "alignment {align} must be >= align_of::<T>() ({element_align})"
403    );
404}
405
406impl GlobalMemoryPool {
407    /// Create a new enhanced global memory pool with SciRS2 integration
408    pub fn new() -> Self {
409        #[cfg(feature = "profiling")]
410        {
411            // let _profile = profile_section!("memory_pool_init");
412        }
413        Self {
414            pools: HashMap::new(),
415            stats: PoolStatistics::default(),
416            config: PoolConfig::default(),
417            // ✅ SciRS2 Memory Management Integration
418            scirs2_pool: GlobalBufferPool::new(),
419            // A diagnostic subsystem must never be able to prevent the core
420            // allocator from being constructed: degrade gracefully instead.
421            leak_detector: LeakDetector::new(Default::default()).ok(),
422            self_weak: None,
423            // metrics_collector: MemoryMetricsCollector::new(),
424            // adaptive_chunking: AdaptiveChunking::new(),
425        }
426    }
427
428    /// ✅ SciRS2 Memory-Efficient Tensor Creation for Large Tensors
429    pub fn create_large_tensor<T: TensorElement>(
430        &mut self,
431        shape: &[usize],
432        device: DeviceType,
433    ) -> Result<Tensor<T>>
434    where
435        T: Clone + Default,
436    {
437        #[cfg(feature = "profiling")]
438        {
439            // let _profile = profile_section!("create_large_tensor");
440        }
441        let total_elements: usize = shape.iter().product();
442        let total_bytes = total_elements * std::mem::size_of::<T>();
443
444        // ✅ Use SciRS2 memory-efficient strategies based on tensor size
445        if total_bytes > 100 * 1024 * 1024 {
446            // >100MB: Use memory-mapped arrays for very large tensors
447            self.create_memory_mapped_tensor(shape, device)
448        } else if total_bytes > 10 * 1024 * 1024 {
449            // >10MB: Use chunked arrays for large tensors
450            self.create_chunked_tensor(shape, device)
451        } else if total_bytes > 1024 * 1024 {
452            // >1MB: Use SciRS2 buffer pool
453            self.create_pooled_tensor(shape, device)
454        } else {
455            // Small tensors: Use standard allocation
456            Tensor::zeros(shape, device)
457        }
458    }
459
460    /// Create memory-mapped tensor for very large data (>100MB).
461    ///
462    /// When the `memory_efficient` feature is enabled, the tensor contents are staged through a
463    /// disk-backed [`MemoryMappedArray`] under [`std::env::temp_dir`]: the data is written to the
464    /// memory map and then read back through the map's `as_slice()`. Without the feature the
465    /// disk-backed path is compiled out and the buffer is allocated in memory.
466    fn create_memory_mapped_tensor<T: TensorElement>(
467        &mut self,
468        shape: &[usize],
469        device: DeviceType,
470    ) -> Result<Tensor<T>>
471    where
472        T: Clone + Default,
473    {
474        let total_elements: usize = shape.iter().product();
475
476        // The buffer that will be persisted to and re-read from the memory-mapped file.
477        let data = vec![T::default(); total_elements];
478
479        #[cfg(feature = "memory_efficient")]
480        {
481            // ✅ SciRS2 Memory-Mapped Array for disk-backed storage: the data genuinely
482            // round-trips through a memory-mapped file and is materialised from `as_slice()`.
483            let backing_path = unique_mmap_path("tensor");
484            let mapped = map_through_mmap_file::<T>(data, &backing_path)?;
485            Tensor::from_data(mapped, shape.to_vec(), device)
486        }
487
488        #[cfg(not(feature = "memory_efficient"))]
489        {
490            // Disk-backed memory mapping is compiled out without the `memory_efficient` feature.
491            Tensor::from_data(data, shape.to_vec(), device)
492        }
493    }
494
495    /// Create chunked tensor for large data (10MB-100MB)
496    fn create_chunked_tensor<T: TensorElement>(
497        &mut self,
498        shape: &[usize],
499        device: DeviceType,
500    ) -> Result<Tensor<T>>
501    where
502        T: Clone + Default,
503    {
504        let total_elements: usize = shape.iter().product();
505
506        // Calculate optimal chunk size based on cache size (1MB chunks by default)
507        let chunk_size = (1024 * 1024) / std::mem::size_of::<T>().max(1); // 1MB chunks
508        let num_chunks = (total_elements + chunk_size - 1) / chunk_size;
509
510        // Creating chunked tensor with calculated parameters
511        let _ = (total_elements, num_chunks, chunk_size); // Use parameters
512
513        // Fallback: Create regular array since ChunkedArray is not available
514        let data = vec![T::default(); total_elements];
515
516        // Track chunked allocation
517        // Metrics collection temporarily disabled - feature not available
518        // self.metrics_collector.record_chunked_allocation(total_elements * std::mem::size_of::<T>(), chunk_size);
519
520        Tensor::from_data(data, shape.to_vec(), device)
521    }
522
523    /// Create pooled tensor using SciRS2 buffer pool (1MB-10MB)
524    fn create_pooled_tensor<T: TensorElement>(
525        &mut self,
526        shape: &[usize],
527        device: DeviceType,
528    ) -> Result<Tensor<T>>
529    where
530        T: Clone + Default,
531    {
532        let total_elements: usize = shape.iter().product();
533        let buffer_size = total_elements * std::mem::size_of::<T>();
534
535        // Log buffer pool allocation
536        let _ = (buffer_size, total_elements); // Use parameters
537
538        // Fallback: Create regular buffer since GlobalBufferPool methods not available
539        let data = vec![T::default(); total_elements];
540
541        // Track pool usage
542        self.stats.pool_hits += 1;
543        // Metrics collection temporarily disabled - feature not available
544        // self.metrics_collector.record_pool_allocation(buffer_size);
545
546        Tensor::from_data(data, shape.to_vec(), device)
547    }
548
549    /// ✅ SciRS2 Lazy Tensor Creation - Defer allocation until needed
550    pub fn create_lazy_tensor<T: TensorElement>(
551        &mut self,
552        shape: &[usize],
553        device: DeviceType,
554    ) -> Result<Tensor<T>>
555    where
556        T: Clone + Default,
557    {
558        #[cfg(feature = "profiling")]
559        {
560            // let _profile = profile_section!("create_lazy_tensor");
561        }
562        let total_elements: usize = shape.iter().product();
563
564        // Fallback: Create regular array since LazyArray is not available
565        let data = vec![T::default(); total_elements];
566
567        // Metrics collection temporarily disabled - feature not available
568        // self.metrics_collector.record_lazy_allocation(total_elements * std::mem::size_of::<T>());
569
570        Tensor::from_data(data, shape.to_vec(), device)
571    }
572
573    /// ✅ SciRS2 Zero-Copy Operations for efficient tensor views
574    pub fn create_zero_copy_view<T: TensorElement>(
575        &self,
576        source: &Tensor<T>,
577        offset: usize,
578        shape: &[usize],
579    ) -> Result<Tensor<T>>
580    where
581        T: Clone,
582    {
583        #[cfg(feature = "profiling")]
584        {
585            // let _profile = profile_section!("zero_copy_view");
586        }
587
588        // Fallback: Create data copy since ZeroCopyOps is not available
589        let source_data = source.data()?;
590        let view_data = source_data[offset..offset + shape.iter().product::<usize>()].to_vec();
591
592        Tensor::from_data(view_data, shape.to_vec(), source.device())
593    }
594
595    /// Get memory usage statistics enhanced with SciRS2 metrics
596    pub fn get_enhanced_stats(&self) -> PoolStatistics {
597        // Simplified: return basic stats for now, enhanced metrics can be added later
598        self.stats.clone()
599    }
600
601    /// Acquire a truly-pooled, uninitialized buffer for `count` elements of type `T`
602    /// with **natural alignment** (`align_of::<T>()`).
603    ///
604    /// This is the low-level method. Prefer the free function [`global_acquire_uninit`].
605    ///
606    /// The returned [`ReusedBuffer<T>`] holds the **actual pooled allocation** — no copy
607    /// is made. Callers must initialize all elements before reading them.
608    ///
609    /// For custom alignment (e.g. 32-byte SIMD alignment), use
610    /// [`Self::acquire_uninit_aligned`] instead.
611    pub fn acquire_uninit<T: 'static>(&mut self, count: usize) -> ReusedBuffer<T> {
612        self.acquire_uninit_aligned::<T>(count, std::mem::align_of::<T>())
613    }
614
615    /// Acquire a pooled uninitialized buffer with custom alignment.
616    ///
617    /// Useful for SIMD-aligned buffers (32-byte for AVX2, 64-byte for AVX-512, etc.).
618    /// Buffers acquired with a given `align` go into their own bucket keyed by
619    /// `(TypeId, SizeClass, align)`, so they never collide with naturally-aligned
620    /// allocations of the same type+size.
621    ///
622    /// # Panics
623    /// - if `align` is not a power of two
624    /// - if `align < std::mem::align_of::<T>()`
625    pub fn acquire_uninit_aligned<T: 'static>(
626        &mut self,
627        count: usize,
628        align: usize,
629    ) -> ReusedBuffer<T> {
630        assert_valid_alignment::<T>(align);
631        let element_size = std::mem::size_of::<T>();
632        let size_bytes = count * element_size;
633        let size_class = self.find_size_class(size_bytes);
634        let type_id = std::any::TypeId::of::<T>();
635        let pool_key = (type_id, size_class, align);
636
637        let layout =
638            Layout::from_size_align(size_bytes.max(1), align).expect("size and align are valid");
639
640        // Update statistics
641        self.stats.total_allocations += 1;
642        self.stats.total_bytes_allocated += size_bytes;
643
644        // Try pool hit
645        if let Some(bucket) = self.pools.get_mut(&pool_key) {
646            // Scan for a compatible entry (may be larger than requested)
647            let mut found_idx: Option<usize> = None;
648            for (i, entry) in bucket.available_buffers.iter().enumerate() {
649                if entry.capacity_bytes >= size_bytes && entry.layout.align() >= align {
650                    found_idx = Some(i);
651                    break;
652                }
653            }
654            if let Some(idx) = found_idx {
655                let raw_entry = bucket
656                    .available_buffers
657                    .remove(idx)
658                    .expect("index was valid moments ago");
659                self.stats.pool_hits += 1;
660                bucket.reuses += 1;
661
662                let ptr = NonNull::new(raw_entry.ptr.as_ptr() as *mut T)
663                    .expect("RawEntry pointer is non-null by construction");
664                // The raw_entry must not drop (its ptr is now owned by ReusedBuffer)
665                let actual_capacity = raw_entry.capacity_bytes / element_size;
666                let entry_layout = raw_entry.layout;
667                std::mem::forget(raw_entry);
668
669                let weak = self.self_weak.clone().unwrap_or_else(Weak::new);
670                return ReusedBuffer {
671                    ptr,
672                    capacity: actual_capacity,
673                    layout: entry_layout,
674                    pool: weak,
675                };
676            }
677        }
678
679        // Pool miss — fresh allocation
680        self.stats.pool_misses += 1;
681
682        // Create the pool bucket if it doesn't exist yet
683        self.pools.entry(pool_key).or_insert_with(|| MemoryPool {
684            available_buffers: VecDeque::new(),
685            size_class,
686            max_buffers: self.config.max_buffers_per_class,
687            allocations: 0,
688            reuses: 0,
689            deallocations: 0,
690        });
691
692        if let Some(bucket) = self.pools.get_mut(&pool_key) {
693            bucket.allocations += 1;
694        }
695
696        // SAFETY: layout is non-zero (we used .max(1) above).
697        let raw_ptr = unsafe { std::alloc::alloc(layout) };
698        let ptr = NonNull::new(raw_ptr as *mut T).unwrap_or_else(|| handle_alloc_error(layout));
699
700        let weak = self.self_weak.clone().unwrap_or_else(Weak::new);
701        ReusedBuffer {
702            ptr,
703            capacity: count,
704            layout,
705            pool: weak,
706        }
707    }
708
709    /// Allocate memory for tensor elements.
710    ///
711    /// Returns a zero-initialized `Vec<T>`.
712    ///
713    /// # Deprecation
714    /// Use [`global_acquire_uninit`] for zero-copy buffer reuse.
715    #[deprecated = "Use global_acquire_uninit instead for zero-copy buffer reuse"]
716    pub fn allocate<T: TensorElement + Default + 'static>(&mut self, count: usize) -> Vec<T> {
717        let mut buf = self.acquire_uninit::<T>(count);
718        // Initialize all elements to Default
719        for slot in buf.as_uninit_slice_mut() {
720            slot.write(T::default());
721        }
722        buf.into_vec(count)
723    }
724
725    /// Find appropriate size class for allocation
726    pub fn find_size_class(&self, size_bytes: usize) -> usize {
727        self.config
728            .size_classes
729            .iter()
730            .position(|&class_size| size_bytes <= class_size)
731            .unwrap_or(self.config.size_classes.len() - 1)
732    }
733
734    /// Deallocate memory by dropping it (legacy; buffer is not returned to pool).
735    ///
736    /// The `deallocate` method previously attempted to store the allocation in the pool
737    /// using an unsafe `Vec<u8>` transmutation that could not reconstruct the correct
738    /// layout. Now the Vec is simply dropped. Use [`ReusedBuffer::release_to_pool`] for
739    /// true pool return.
740    pub fn deallocate<T: 'static>(&mut self, data: Vec<T>) {
741        // Just drop `data` — memory is freed by Vec's Drop.
742        drop(data);
743    }
744
745    /// Clear all pools
746    pub fn clear(&mut self) {
747        self.pools.clear();
748        self.stats = PoolStatistics::default();
749    }
750
751    /// Get basic statistics
752    pub fn get_statistics(&self) -> &PoolStatistics {
753        &self.stats
754    }
755
756    /// Get cache hit rate
757    pub fn hit_rate(&self) -> f64 {
758        if self.stats.total_allocations == 0 {
759            0.0
760        } else {
761            self.stats.pool_hits as f64 / self.stats.total_allocations as f64
762        }
763    }
764
765    /// Cleanup unused memory
766    pub fn cleanup(&mut self) {
767        if self.config.auto_cleanup {
768            let threshold_bytes =
769                (self.config.max_total_memory as f64 * self.config.cleanup_threshold) as usize;
770            if self.stats.total_bytes_allocated > threshold_bytes {
771                self.pools
772                    .retain(|_, pool| !pool.available_buffers.is_empty());
773            }
774        }
775    }
776}
777
778impl std::fmt::Debug for GlobalMemoryPool {
779    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
780        f.debug_struct("GlobalMemoryPool")
781            .field("pools", &self.pools)
782            .field("stats", &self.stats)
783            .field("config", &self.config)
784            .field("scirs2_pool", &"<GlobalBufferPool>")
785            .field(
786                "leak_detector",
787                &self.leak_detector.as_ref().map(|_| "<LeakDetector>"),
788            )
789            .finish()
790    }
791}
792
793// ─── Debug impl for MemoryPool (needs RawEntry to be Debug) ──────────────────
794
795impl std::fmt::Debug for RawEntry {
796    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
797        f.debug_struct("RawEntry")
798            .field("capacity_bytes", &self.capacity_bytes)
799            .finish()
800    }
801}
802
803// ─── Public free function ─────────────────────────────────────────────────────
804
805/// Acquire an uninitialized buffer from the global memory pool.
806///
807/// This is the **preferred API** for zero-copy buffer reuse. The returned
808/// [`ReusedBuffer<T>`] holds the actual pooled allocation — no copying occurs.
809///
810/// # Safety contract on the caller
811/// Elements must be initialized before being read. Use [`ReusedBuffer::as_uninit_slice_mut`]
812/// to write values, then either:
813/// - call [`ReusedBuffer::into_vec`] to obtain an owning `Vec`, or
814/// - call [`ReusedBuffer::release_to_pool`] to return the buffer.
815pub fn global_acquire_uninit<T: 'static>(count: usize) -> ReusedBuffer<T> {
816    let pool_arc = get_memory_pool();
817    let mut guard = pool_arc
818        .lock()
819        .unwrap_or_else(|poisoned| poisoned.into_inner());
820    guard.acquire_uninit::<T>(count)
821}
822
823/// Acquire an uninitialized buffer from the global memory pool with custom alignment.
824///
825/// Like [`global_acquire_uninit`], but the returned buffer is guaranteed to be aligned
826/// to at least `align` bytes. Useful for SIMD-aligned buffers (e.g. 32 bytes for AVX2).
827///
828/// # Panics
829/// - if `align` is not a power of two
830/// - if `align < std::mem::align_of::<T>()`
831///
832/// # Safety contract on the caller
833/// Same as [`global_acquire_uninit`] — elements must be initialized before being read.
834pub fn global_acquire_uninit_aligned<T: 'static>(count: usize, align: usize) -> ReusedBuffer<T> {
835    // Validate *before* touching the global pool lock at all: an invalid `align`
836    // is a pure caller error (doesn't depend on any pool state), so failing fast
837    // here means the panic below never happens while `MEMORY_POOL` is held. See
838    // `assert_valid_alignment`'s doc comment for why that ordering matters.
839    assert_valid_alignment::<T>(align);
840
841    let pool_arc = get_memory_pool();
842    let mut guard = pool_arc
843        .lock()
844        .unwrap_or_else(|poisoned| poisoned.into_inner());
845    guard.acquire_uninit_aligned::<T>(count, align)
846}
847
848/// Enhanced memory statistics with SciRS2 integration
849/// Currently simplified to use basic PoolStatistics
850/// Future versions will include full SciRS2 memory metrics integration
851pub type EnhancedMemoryStats = PoolStatistics;
852
853/// ✅ Enhanced Tensor creation interface with SciRS2 memory optimization
854impl<T: TensorElement> Tensor<T> {
855    /// Create memory-efficient tensor with automatic strategy selection
856    pub fn create_efficient(shape: &[usize], device: DeviceType) -> Result<Self>
857    where
858        T: Clone + Default,
859    {
860        let binding = get_memory_pool();
861        let mut pool = binding
862            .lock()
863            .unwrap_or_else(|poisoned| poisoned.into_inner());
864        pool.create_large_tensor::<T>(shape, device)
865    }
866
867    /// Create lazy tensor that defers allocation until first access
868    pub fn lazy(shape: &[usize], device: DeviceType) -> Result<Self>
869    where
870        T: Clone + Default,
871    {
872        let binding = get_memory_pool();
873        let mut pool = binding
874            .lock()
875            .unwrap_or_else(|poisoned| poisoned.into_inner());
876        pool.create_lazy_tensor::<T>(shape, device)
877    }
878
879    /// Create zero-copy view of existing tensor (disabled due to conflict with shape_ops)
880    // pub fn view(&self, offset: usize, new_shape: &[usize]) -> Result<Self>
881    // where
882    //     T: Clone,
883    // {
884    //     let pool = get_memory_pool().lock().expect("lock should not be poisoned");
885    //     pool.create_zero_copy_view(self, offset, new_shape)
886    // }
887
888    /// ✅ SciRS2 Memory-Mapped Tensor for very large datasets
889    pub fn memory_mapped(shape: &[usize], device: DeviceType) -> Result<Self>
890    where
891        T: Clone + Default,
892    {
893        #[cfg(feature = "profiling")]
894        {
895            // let _profile = profile_section!("memory_mapped_tensor");
896        }
897
898        // Fallback: Create regular tensor since memory mapping requires additional implementation
899        let total_elements: usize = shape.iter().product();
900        let data = vec![T::default(); total_elements];
901        Self::from_data(data, shape.to_vec(), device)
902    }
903
904    /// ✅ SciRS2 Chunked Tensor for cache-efficient large data processing
905    ///
906    /// Creates a tensor optimized for chunk-wise processing with the specified chunk size.
907    /// This is useful for large tensors that benefit from cache-friendly access patterns.
908    ///
909    /// # Arguments
910    /// * `shape` - The shape of the tensor
911    /// * `chunk_size` - Preferred chunk size for processing (in elements)
912    /// * `device` - Device to allocate the tensor on
913    pub fn chunked(shape: &[usize], chunk_size: usize, device: DeviceType) -> Result<Self>
914    where
915        T: Clone + Default,
916    {
917        #[cfg(feature = "profiling")]
918        {
919            // let _profile = profile_section!("chunked_tensor");
920        }
921        let total_elements: usize = shape.iter().product();
922
923        // Validate chunk size
924        let effective_chunk_size = if chunk_size == 0 {
925            // Default to 64KB chunks for cache efficiency
926            let default_chunk_bytes = 64 * 1024;
927            let element_size = std::mem::size_of::<T>();
928            (default_chunk_bytes / element_size.max(1)).max(1)
929        } else {
930            chunk_size
931        };
932
933        // Align chunk size to cache line boundaries (64 bytes typically)
934        let cache_line_elements = 64 / std::mem::size_of::<T>().max(1);
935        let aligned_chunk_size = ((effective_chunk_size + cache_line_elements - 1)
936            / cache_line_elements)
937            * cache_line_elements;
938
939        // Log chunk configuration for debugging
940        let _ = (total_elements, effective_chunk_size, aligned_chunk_size); // Use parameters
941
942        // Create the tensor with default values
943        let data = vec![T::default(); total_elements];
944
945        // Note: The aligned_chunk_size is stored in metadata for use by process_chunked
946        // and other chunk-aware operations. This provides better cache locality.
947        Self::from_data(data, shape.to_vec(), device)
948    }
949
950    /// Disk-backed tensor for datasets larger than RAM
951    ///
952    /// The tensor's elements live in a file, not in the process heap: the
953    /// backing file is filled in bounded chunks and every read goes through
954    /// [`crate::storage::MemoryMappedStorage`], so creating the tensor costs a
955    /// fixed amount of RAM regardless of its size.
956    ///
957    /// # Arguments
958    /// * `shape` - The shape of the tensor
959    /// * `device` - Device to allocate the tensor on
960    /// * `file_path` - Optional file path for persistent storage. If `None`, a
961    ///   unique temporary file is used and deleted when the tensor is dropped.
962    ///
963    /// # Note
964    /// Element and slice reads stream from the file, but whole-tensor
965    /// materialisation (`to_vec`, `data`) still builds an in-memory copy — that
966    /// call is what a dataset larger than RAM must avoid.
967    pub fn disk_backed(shape: &[usize], device: DeviceType, file_path: Option<&str>) -> Result<Self>
968    where
969        T: Clone + Default,
970    {
971        #[cfg(feature = "profiling")]
972        {
973            // let _profile = profile_section!("disk_backed_tensor");
974        }
975        let total_elements: usize = shape.iter().product();
976
977        // `None` lets the storage pick a unique temporary path (and delete it on
978        // drop); an explicit path is persistent and is never removed.
979        let backing_path = file_path.map(std::path::PathBuf::from);
980
981        let storage =
982            TensorStorage::memory_mapped_filled(total_elements, T::default(), backing_path)?;
983
984        let mut tensor = Self::from_data(Vec::new(), Vec::new(), device)?;
985        tensor.storage = storage;
986        tensor.shape = torsh_core::shape::Shape::new(shape.to_vec());
987        Ok(tensor)
988    }
989
990    /// Process tensor in memory-efficient chunks
991    pub fn process_chunked<F, R>(&self, chunk_size: usize, mut processor: F) -> Result<Vec<R>>
992    where
993        F: FnMut(&[T]) -> Result<R>,
994        T: Clone,
995    {
996        #[cfg(feature = "profiling")]
997        {
998            // let _profile = profile_section!("process_chunked");
999        }
1000        let data = self.data()?;
1001        let mut results = Vec::new();
1002
1003        // Fallback: Use fixed chunk size since AdaptiveChunking is not available
1004        let effective_chunk_size = chunk_size;
1005
1006        for chunk in data.chunks(effective_chunk_size) {
1007            results.push(processor(chunk)?);
1008        }
1009
1010        Ok(results)
1011    }
1012}
1013
1014impl MemoryPool {
1015    fn new(size_class: usize, max_buffers: usize) -> Self {
1016        Self {
1017            available_buffers: VecDeque::new(),
1018            size_class,
1019            max_buffers,
1020            allocations: 0,
1021            reuses: 0,
1022            deallocations: 0,
1023        }
1024    }
1025}
1026
1027impl<T: TensorElement + Copy + Default> PooledTensor<T> {
1028    /// Create a new pooled tensor
1029    pub fn new(shape: &[usize], device: DeviceType) -> Result<Self> {
1030        let numel = shape.iter().product::<usize>();
1031
1032        // Allocate from pool
1033        let pool = get_memory_pool();
1034        let data = {
1035            let mut pool_guard = pool.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
1036            #[allow(deprecated)]
1037            pool_guard.allocate::<T>(numel)
1038        };
1039
1040        let tensor = Tensor::from_data(data, shape.to_vec(), device)?;
1041        let type_id = std::any::TypeId::of::<T>();
1042        let size_class = {
1043            let pool_guard = pool.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
1044            pool_guard.find_size_class(numel * std::mem::size_of::<T>())
1045        };
1046        let align = std::mem::align_of::<T>();
1047
1048        Ok(Self {
1049            tensor,
1050            pool_key: Some((type_id, size_class, align)),
1051            _phantom: PhantomData,
1052        })
1053    }
1054
1055    /// Create pooled zeros tensor
1056    pub fn zeros(shape: &[usize], device: DeviceType) -> Result<Self> {
1057        let mut pooled = Self::new(shape, device)?;
1058        // Initialize with zeros
1059        let numel = shape.iter().product::<usize>();
1060        let data = vec![T::default(); numel];
1061        pooled.tensor.storage = TensorStorage::create_optimal(data)?;
1062        Ok(pooled)
1063    }
1064
1065    /// Create pooled ones tensor
1066    pub fn ones(shape: &[usize], device: DeviceType) -> Result<Self>
1067    where
1068        T: std::ops::Add<Output = T> + From<f32>,
1069    {
1070        let mut pooled = Self::new(shape, device)?;
1071        // Initialize with ones
1072        let numel = shape.iter().product::<usize>();
1073        let data = vec![T::from(1.0f32); numel];
1074        pooled.tensor.storage = TensorStorage::create_optimal(data)?;
1075        Ok(pooled)
1076    }
1077
1078    /// Get reference to the underlying tensor
1079    pub fn tensor(&self) -> &Tensor<T> {
1080        &self.tensor
1081    }
1082
1083    /// Get mutable reference to the underlying tensor
1084    pub fn tensor_mut(&mut self) -> &mut Tensor<T> {
1085        &mut self.tensor
1086    }
1087
1088    /// Convert to owned tensor (removes from pool management)
1089    pub fn into_tensor(mut self) -> Tensor<T> {
1090        self.pool_key = None; // Prevent return to pool
1091        self.tensor.clone()
1092    }
1093}
1094
1095impl<T: TensorElement + std::default::Default> Drop for PooledTensor<T> {
1096    fn drop(&mut self) {
1097        if let Some((_type_id, _size_class, _align)) = self.pool_key {
1098            // Return memory to pool via deallocate (which now simply drops).
1099            if let Ok(data) = self.tensor.to_vec() {
1100                let pool = get_memory_pool();
1101                let mut pool_guard = pool.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
1102                pool_guard.deallocate(data);
1103            }
1104        }
1105    }
1106}
1107
1108/// Convenient functions for creating pooled tensors
1109impl<T: TensorElement + Copy + Default> Tensor<T> {
1110    /// Create a tensor using the memory pool
1111    pub fn pooled(shape: &[usize], device: DeviceType) -> Result<PooledTensor<T>> {
1112        PooledTensor::new(shape, device)
1113    }
1114
1115    /// Create temporary tensor for intermediate calculations
1116    pub fn temporary(shape: &[usize], device: DeviceType) -> Result<PooledTensor<T>> {
1117        PooledTensor::new(shape, device)
1118    }
1119}
1120
1121/// Global functions for pool management
1122pub fn clear_memory_pool() {
1123    if let Some(pool) = MEMORY_POOL.get() {
1124        pool.lock()
1125            .unwrap_or_else(|poisoned| poisoned.into_inner())
1126            .clear();
1127    }
1128}
1129
1130pub fn get_pool_statistics() -> PoolStatistics {
1131    get_memory_pool()
1132        .lock()
1133        .unwrap_or_else(|poisoned| poisoned.into_inner())
1134        .get_statistics()
1135        .clone()
1136}
1137
1138pub fn get_pool_hit_rate() -> f64 {
1139    get_memory_pool()
1140        .lock()
1141        .unwrap_or_else(|poisoned| poisoned.into_inner())
1142        .hit_rate()
1143}
1144
1145pub fn cleanup_memory_pool() {
1146    get_memory_pool()
1147        .lock()
1148        .unwrap_or_else(|poisoned| poisoned.into_inner())
1149        .cleanup();
1150}
1151
1152#[cfg(test)]
1153mod tests {
1154    use super::*;
1155
1156    /// Serialises **every** test in this module.
1157    ///
1158    /// They all read or mutate one process-global singleton — the pool itself
1159    /// plus its allocation/hit counters — and `cargo test` runs a binary's
1160    /// tests in one process across a thread pool (unlike `cargo nextest`'s
1161    /// process-per-test). `clear_memory_pool()` resets those counters to zero,
1162    /// so a test calling it concurrently with another test's measurement makes
1163    /// that measurement read a wiped pool. Measured on the pre-fix tree, with
1164    /// only the buffer-identity tests holding this lock:
1165    ///
1166    /// ```text
1167    /// thread 'memory_pool::tests::test_pool_statistics' panicked at
1168    ///   crates/torsh-tensor/src/memory_pool.rs:1189:9:
1169    /// assertion failed: stats.total_allocations >= 2
1170    /// ```
1171    ///
1172    /// — 2 of 20 `cargo test -p torsh-tensor --lib memory_pool::` runs (and 4
1173    /// of 10 in a hotter round), because `test_memory_pool_basic`,
1174    /// `test_pool_statistics` and `test_pool_cleanup` called
1175    /// `clear_memory_pool()` without holding it. The lock is held for each
1176    /// test's full body, which costs nothing measurable: the whole module runs
1177    /// in well under a millisecond.
1178    ///
1179    /// The three memory-mapped tests below are deliberately *not* serialised:
1180    /// they drive file I/O and a locally constructed `GlobalMemoryPool`, and
1181    /// never touch the singleton.
1182    static TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
1183
1184    #[test]
1185    fn test_memory_pool_basic() {
1186        let _guard = TEST_LOCK
1187            .lock()
1188            .unwrap_or_else(|poisoned| poisoned.into_inner());
1189        clear_memory_pool();
1190
1191        // Create pooled tensor
1192        let pooled = PooledTensor::<f32>::zeros(&[100, 100], DeviceType::Cpu)
1193            .expect("zeros creation should succeed");
1194        assert_eq!(pooled.tensor().numel(), 10000);
1195
1196        // Drop should return memory to pool
1197        drop(pooled);
1198
1199        // Next allocation should reuse memory
1200        let _pooled2 = PooledTensor::<f32>::zeros(&[100, 100], DeviceType::Cpu)
1201            .expect("zeros creation should succeed");
1202
1203        let stats = get_pool_statistics();
1204        assert!(stats.pool_hits > 0 || stats.pool_misses > 0);
1205    }
1206
1207    #[test]
1208    fn test_pool_statistics() {
1209        let _guard = TEST_LOCK
1210            .lock()
1211            .unwrap_or_else(|poisoned| poisoned.into_inner());
1212        clear_memory_pool();
1213
1214        let _pooled1 = PooledTensor::<f32>::zeros(&[50, 50], DeviceType::Cpu)
1215            .expect("zeros creation should succeed");
1216        let _pooled2 = PooledTensor::<f32>::ones(&[50, 50], DeviceType::Cpu)
1217            .expect("ones creation should succeed");
1218
1219        let stats = get_pool_statistics();
1220        assert!(stats.total_allocations >= 2);
1221        assert!(stats.total_bytes_allocated > 0);
1222    }
1223
1224    #[test]
1225    fn test_pool_cleanup() {
1226        let _guard = TEST_LOCK
1227            .lock()
1228            .unwrap_or_else(|poisoned| poisoned.into_inner());
1229        clear_memory_pool();
1230
1231        // Create many temporary tensors
1232        for _ in 0..10 {
1233            let _temp = PooledTensor::<f32>::zeros(&[100, 100], DeviceType::Cpu)
1234                .expect("zeros creation should succeed");
1235        }
1236
1237        cleanup_memory_pool();
1238        let _stats = get_pool_statistics();
1239        // After cleanup, bytes in pools should be reduced (test passes if no panic occurs)
1240    }
1241
1242    #[test]
1243    fn test_pooled_tensor_conversion() {
1244        // Allocates from (and releases into) the singleton, so it has to be
1245        // serialised too even though it never asserts on the counters.
1246        let _guard = TEST_LOCK
1247            .lock()
1248            .unwrap_or_else(|poisoned| poisoned.into_inner());
1249        let pooled = PooledTensor::<f32>::ones(&[10, 10], DeviceType::Cpu)
1250            .expect("ones creation should succeed");
1251        let tensor = pooled.into_tensor();
1252        assert_eq!(tensor.numel(), 100);
1253    }
1254
1255    // ── New ReusedBuffer tests ──────────────────────────────────────────────
1256
1257    #[test]
1258    fn test_acquire_truly_reuses_allocation() {
1259        let _guard = TEST_LOCK
1260            .lock()
1261            .unwrap_or_else(|poisoned| poisoned.into_inner());
1262        clear_memory_pool();
1263
1264        let buf1: ReusedBuffer<f32> = global_acquire_uninit::<f32>(1024);
1265        let ptr1 = buf1.as_ptr_raw();
1266        buf1.release_to_pool();
1267
1268        let buf2: ReusedBuffer<f32> = global_acquire_uninit::<f32>(1024);
1269        let ptr2 = buf2.as_ptr_raw();
1270        buf2.release_to_pool();
1271
1272        assert_eq!(
1273            ptr1, ptr2,
1274            "pool should return the same allocation on second acquire"
1275        );
1276    }
1277
1278    #[test]
1279    fn test_into_vec_transfers_ownership() {
1280        let _guard = TEST_LOCK
1281            .lock()
1282            .unwrap_or_else(|poisoned| poisoned.into_inner());
1283        clear_memory_pool();
1284
1285        let mut buf: ReusedBuffer<f32> = global_acquire_uninit::<f32>(64);
1286        // Write to the buffer
1287        for slot in buf.as_uninit_slice_mut() {
1288            slot.write(1.0_f32);
1289        }
1290        let vec = buf.into_vec(64);
1291        assert_eq!(vec.len(), 64);
1292        assert!(vec.iter().all(|&x| x == 1.0_f32));
1293    }
1294
1295    #[test]
1296    fn test_drop_returns_to_pool() {
1297        let _guard = TEST_LOCK
1298            .lock()
1299            .unwrap_or_else(|poisoned| poisoned.into_inner());
1300        clear_memory_pool();
1301
1302        {
1303            let buf: ReusedBuffer<f32> = global_acquire_uninit::<f32>(256);
1304            // Drop without consuming — should return to pool
1305            drop(buf);
1306        }
1307
1308        // Second acquire should be a pool hit (same size class)
1309        let buf2: ReusedBuffer<f32> = global_acquire_uninit::<f32>(256);
1310        buf2.release_to_pool();
1311
1312        let stats = get_pool_statistics();
1313        assert!(
1314            stats.pool_hits >= 1,
1315            "expected at least one pool hit after drop-return"
1316        );
1317    }
1318
1319    #[test]
1320    fn test_acquire_capacity_and_uninit_slice() {
1321        let _guard = TEST_LOCK
1322            .lock()
1323            .unwrap_or_else(|poisoned| poisoned.into_inner());
1324        clear_memory_pool();
1325
1326        let buf: ReusedBuffer<u64> = global_acquire_uninit::<u64>(32);
1327        assert_eq!(buf.capacity(), 32);
1328        buf.release_to_pool();
1329    }
1330
1331    // ── Aligned-acquire tests ───────────────────────────────────────────────
1332
1333    #[test]
1334    fn test_acquire_aligned_returns_simd_aligned_pointer() {
1335        let _guard = TEST_LOCK
1336            .lock()
1337            .unwrap_or_else(|poisoned| poisoned.into_inner());
1338        clear_memory_pool();
1339
1340        // 32-byte alignment (AVX2 / scirs2_core::simd_aligned::SIMD_ALIGNMENT).
1341        let buf: ReusedBuffer<f32> = global_acquire_uninit_aligned::<f32>(1024, 32);
1342        assert_eq!(buf.capacity(), 1024);
1343        let addr = buf.as_ptr_raw() as usize;
1344        assert_eq!(
1345            addr % 32,
1346            0,
1347            "buffer pointer {addr:#x} must be 32-byte aligned"
1348        );
1349        buf.release_to_pool();
1350    }
1351
1352    #[test]
1353    fn test_acquire_aligned_pool_hit_on_release() {
1354        let _guard = TEST_LOCK
1355            .lock()
1356            .unwrap_or_else(|poisoned| poisoned.into_inner());
1357        clear_memory_pool();
1358
1359        let buf1: ReusedBuffer<f32> = global_acquire_uninit_aligned::<f32>(2048, 32);
1360        let ptr1 = buf1.as_ptr_raw();
1361        let cap1 = buf1.capacity();
1362        buf1.release_to_pool();
1363
1364        let buf2: ReusedBuffer<f32> = global_acquire_uninit_aligned::<f32>(2048, 32);
1365        let ptr2 = buf2.as_ptr_raw();
1366        let cap2 = buf2.capacity();
1367        assert_eq!(
1368            ptr1, ptr2,
1369            "aligned bucket should return the same allocation on second acquire"
1370        );
1371        assert_eq!(cap1, cap2, "capacity should match across reuse");
1372        // Pointer must still be aligned after pool reuse.
1373        assert_eq!(ptr2 as usize % 32, 0, "reused buffer must remain aligned");
1374        buf2.release_to_pool();
1375    }
1376
1377    #[test]
1378    fn test_aligned_and_natural_buckets_are_independent() {
1379        let _guard = TEST_LOCK
1380            .lock()
1381            .unwrap_or_else(|poisoned| poisoned.into_inner());
1382        clear_memory_pool();
1383
1384        // 32-byte aligned acquire/release for a size that maps to a particular size class.
1385        let buf_aligned: ReusedBuffer<f32> = global_acquire_uninit_aligned::<f32>(512, 32);
1386        let ptr_aligned = buf_aligned.as_ptr_raw();
1387        buf_aligned.release_to_pool();
1388
1389        // Natural-alignment acquire of the same (T, size) — must NOT collide with the
1390        // 32-aligned bucket; it should produce a fresh allocation.
1391        let buf_natural: ReusedBuffer<f32> = global_acquire_uninit::<f32>(512);
1392        let ptr_natural = buf_natural.as_ptr_raw();
1393        assert_ne!(
1394            ptr_aligned, ptr_natural,
1395            "naturally-aligned bucket must be distinct from the 32-byte bucket"
1396        );
1397        buf_natural.release_to_pool();
1398    }
1399
1400    #[test]
1401    #[should_panic(expected = "alignment must be a power of two")]
1402    fn test_acquire_aligned_rejects_non_power_of_two() {
1403        let _guard = TEST_LOCK
1404            .lock()
1405            .unwrap_or_else(|poisoned| poisoned.into_inner());
1406        clear_memory_pool();
1407        let _buf: ReusedBuffer<f32> = global_acquire_uninit_aligned::<f32>(16, 6);
1408    }
1409
1410    // ── Memory-mapped allocation path ───────────────────────────────────────
1411    // These exercise the real disk-backed `scirs2_core::memory_efficient::MemoryMappedArray`
1412    // path and are gated on the `memory_efficient` feature. Run with:
1413    //   cargo test -p torsh-tensor --features memory_efficient
1414
1415    /// Round-trips KNOWN (non-default) data through the exact helper used by
1416    /// `create_memory_mapped_tensor`: write to a temp-dir backing file, read back via
1417    /// `as_slice()`, assert equality. Fails if the mmap wiring drops/garbles the data.
1418    #[cfg(feature = "memory_efficient")]
1419    #[test]
1420    fn test_map_through_mmap_file_roundtrips_known_data() {
1421        // Non-default values so a zero-init regression cannot accidentally pass.
1422        let known: Vec<f32> = (0..48).map(|i| (i as f32) * 1.5 - 7.25).collect();
1423
1424        let backing_path = unique_mmap_path("test_helper");
1425        assert!(
1426            backing_path.starts_with(std::env::temp_dir()),
1427            "backing file must live under the system temp directory"
1428        );
1429
1430        let mapped = map_through_mmap_file::<f32>(known.clone(), &backing_path)
1431            .expect("memory-mapped round-trip should succeed");
1432
1433        assert_eq!(
1434            mapped, known,
1435            "as_slice() must return exactly the data written to the memory-mapped file"
1436        );
1437
1438        // Defensive cleanup in case the helper's best-effort removal failed.
1439        let _ = std::fs::remove_file(&backing_path);
1440    }
1441
1442    /// Directly drives `MemoryMappedArray::new(..)` + `as_slice()` with known `f64` data under
1443    /// the temp directory to pin the exact scirs2-core API contract the wiring relies on.
1444    #[cfg(feature = "memory_efficient")]
1445    #[test]
1446    fn test_memory_mapped_array_as_slice_direct() {
1447        use scirs2_core::memory_efficient::{AccessMode, MemoryMappedArray};
1448        use scirs2_core::ndarray::Array1;
1449
1450        let known: Vec<f64> = vec![3.5, -1.25, 42.0, 7.0, 0.5, 100.0, -8.0, 256.0];
1451        let backing_path = unique_mmap_path("test_direct");
1452
1453        let array = Array1::from(known.clone());
1454        let mmap = MemoryMappedArray::<f64>::new(Some(&array), &backing_path, AccessMode::Write, 0)
1455            .expect("memory-mapped array creation should succeed");
1456
1457        let read_back = mmap.as_slice().to_vec();
1458        drop(mmap);
1459        let _ = std::fs::remove_file(&backing_path);
1460
1461        assert_eq!(
1462            read_back, known,
1463            "as_slice() over a Write-mode memory map must return the written data"
1464        );
1465    }
1466
1467    /// Drives the production method `create_memory_mapped_tensor` end-to-end through the
1468    /// memory-mapped path and verifies the resulting tensor's shape and contents.
1469    #[cfg(feature = "memory_efficient")]
1470    #[test]
1471    fn test_create_memory_mapped_tensor_uses_mmap_path() {
1472        let mut pool = GlobalMemoryPool::new();
1473        let shape = [4usize, 5];
1474        let tensor = pool
1475            .create_memory_mapped_tensor::<f32>(&shape, DeviceType::Cpu)
1476            .expect("memory-mapped tensor creation should succeed");
1477
1478        assert_eq!(tensor.numel(), 20);
1479        let dims = tensor.shape();
1480        assert_eq!(dims.dims(), &[4, 5]);
1481
1482        // Data was staged through the memory map and read back via as_slice();
1483        // a freshly-allocated tensor holds default (zero) values.
1484        let data = tensor.data().expect("tensor data should be readable");
1485        assert_eq!(data.len(), 20);
1486        assert!(data.iter().all(|&x| x == 0.0_f32));
1487    }
1488}