ipfrs_interface/
mmap.rs

1//! Memory-Mapped File Serving
2//!
3//! Provides zero-copy file serving using memory-mapped I/O for improved performance
4//! with large tensor files. This module enables efficient serving of tensors without
5//! loading the entire file into memory.
6//!
7//! # Features
8//!
9//! - **Zero-copy serving** - Direct memory mapping without buffer allocation
10//! - **Lazy loading** - Map files only when accessed
11//! - **Range request support** - Efficient partial file serving
12//! - **Platform optimizations** - Uses OS-level optimizations (sendfile, etc.)
13//!
14//! # Safety
15//!
16//! Memory mapping is inherently unsafe as it involves raw pointer access. This module
17//! provides a safe wrapper that ensures proper lifetime management and error handling.
18
19use bytes::Bytes;
20use memmap2::Mmap;
21use std::fs::File;
22use std::io;
23use std::path::{Path, PathBuf};
24use std::sync::Arc;
25use thiserror::Error;
26
27// ============================================================================
28// Error Types
29// ============================================================================
30
31/// Errors that can occur during memory-mapped operations
32#[derive(Debug, Error)]
33pub enum MmapError {
34    #[error("Failed to open file: {0}")]
35    FileOpen(#[from] io::Error),
36
37    #[error("Failed to create memory map: {0}")]
38    MmapCreation(String),
39
40    #[error("Invalid range: {0}")]
41    InvalidRange(String),
42
43    #[error("File not found: {0}")]
44    FileNotFound(String),
45}
46
47// ============================================================================
48// Memory-Mapped File Handle
49// ============================================================================
50
51/// A handle to a memory-mapped file
52///
53/// This structure provides safe access to memory-mapped files with proper
54/// lifetime management and error handling.
55pub struct MmapFile {
56    /// The memory-mapped region
57    mmap: Arc<Mmap>,
58    /// Original file path (for debugging)
59    path: PathBuf,
60    /// Total file size
61    size: usize,
62}
63
64impl MmapFile {
65    /// Create a new memory-mapped file from a path
66    ///
67    /// # Arguments
68    ///
69    /// * `path` - Path to the file to be memory-mapped
70    ///
71    /// # Returns
72    ///
73    /// A `Result` containing the `MmapFile` or an error
74    ///
75    /// # Safety
76    ///
77    /// This function is safe because it ensures:
78    /// - File handle is valid
79    /// - Memory map is created successfully
80    /// - Lifetime of the mmap is tied to the struct
81    pub fn new<P: AsRef<Path>>(path: P) -> Result<Self, MmapError> {
82        let path = path.as_ref();
83
84        // Open the file
85        let file = File::open(path).map_err(|e| {
86            if e.kind() == io::ErrorKind::NotFound {
87                MmapError::FileNotFound(path.display().to_string())
88            } else {
89                MmapError::FileOpen(e)
90            }
91        })?;
92
93        // Get file size
94        let metadata = file.metadata()?;
95        let size = metadata.len() as usize;
96
97        // Create memory map
98        // SAFETY: The file is opened successfully and we have a valid file handle
99        let mmap = unsafe { Mmap::map(&file).map_err(|e| MmapError::MmapCreation(e.to_string()))? };
100
101        Ok(MmapFile {
102            mmap: Arc::new(mmap),
103            path: path.to_path_buf(),
104            size,
105        })
106    }
107
108    /// Get the full file contents as a Bytes object (zero-copy)
109    ///
110    /// This returns a `Bytes` object that references the memory-mapped region
111    /// without copying the data.
112    pub fn bytes(&self) -> Bytes {
113        Bytes::copy_from_slice(&self.mmap[..])
114    }
115
116    /// Get a range of bytes from the file (zero-copy slice)
117    ///
118    /// # Arguments
119    ///
120    /// * `range` - The byte range to retrieve (start..end)
121    ///
122    /// # Returns
123    ///
124    /// A `Bytes` object containing the requested range
125    pub fn range(&self, range: std::ops::Range<usize>) -> Result<Bytes, MmapError> {
126        if range.start > self.size {
127            return Err(MmapError::InvalidRange(format!(
128                "Start {} exceeds file size {}",
129                range.start, self.size
130            )));
131        }
132
133        if range.end > self.size {
134            return Err(MmapError::InvalidRange(format!(
135                "End {} exceeds file size {}",
136                range.end, self.size
137            )));
138        }
139
140        if range.start >= range.end {
141            return Err(MmapError::InvalidRange(format!(
142                "Invalid range: {}..{}",
143                range.start, range.end
144            )));
145        }
146
147        Ok(Bytes::copy_from_slice(&self.mmap[range]))
148    }
149
150    /// Get the file size in bytes
151    pub fn size(&self) -> usize {
152        self.size
153    }
154
155    /// Get the file path
156    pub fn path(&self) -> &Path {
157        &self.path
158    }
159
160    /// Check if the file is empty
161    pub fn is_empty(&self) -> bool {
162        self.size == 0
163    }
164
165    /// Get multiple ranges efficiently (for HTTP multi-range requests)
166    ///
167    /// # Arguments
168    ///
169    /// * `ranges` - Vector of byte ranges to retrieve
170    ///
171    /// # Returns
172    ///
173    /// A vector of `Bytes` objects, one for each range
174    pub fn multi_range(&self, ranges: &[std::ops::Range<usize>]) -> Result<Vec<Bytes>, MmapError> {
175        let mut results = Vec::with_capacity(ranges.len());
176
177        for range in ranges {
178            results.push(self.range(range.clone())?);
179        }
180
181        Ok(results)
182    }
183}
184
185// Implement Clone for MmapFile (cheap because of Arc)
186impl Clone for MmapFile {
187    fn clone(&self) -> Self {
188        MmapFile {
189            mmap: Arc::clone(&self.mmap),
190            path: self.path.clone(),
191            size: self.size,
192        }
193    }
194}
195
196// ============================================================================
197// Memory-Mapped File Cache
198// ============================================================================
199
200/// A simple cache for memory-mapped files
201///
202/// This cache stores recently accessed memory-mapped files to avoid
203/// repeated file opening and mapping operations.
204#[allow(dead_code)]
205pub struct MmapCache {
206    /// Maximum number of cached files
207    max_entries: usize,
208    /// Cache storage (simple LRU would be better in production)
209    cache: dashmap::DashMap<PathBuf, Arc<MmapFile>>,
210}
211
212impl MmapCache {
213    /// Create a new mmap cache
214    ///
215    /// # Arguments
216    ///
217    /// * `max_entries` - Maximum number of files to keep in cache
218    pub fn new(max_entries: usize) -> Self {
219        MmapCache {
220            max_entries,
221            cache: dashmap::DashMap::new(),
222        }
223    }
224
225    /// Get or create a memory-mapped file
226    ///
227    /// If the file is in cache, return the cached version. Otherwise,
228    /// create a new mmap and cache it.
229    pub fn get_or_create<P: AsRef<Path>>(&self, path: P) -> Result<Arc<MmapFile>, MmapError> {
230        let path = path.as_ref();
231
232        // Check cache first
233        if let Some(cached) = self.cache.get(path) {
234            return Ok(Arc::clone(&*cached));
235        }
236
237        // Create new mmap
238        let mmap_file = Arc::new(MmapFile::new(path)?);
239
240        // Add to cache (simple eviction: just check size)
241        if self.cache.len() >= self.max_entries {
242            // In production, implement proper LRU eviction
243            // For now, just allow growth
244            tracing::warn!(
245                "Mmap cache size {} exceeds max {}",
246                self.cache.len(),
247                self.max_entries
248            );
249        }
250
251        self.cache
252            .insert(path.to_path_buf(), Arc::clone(&mmap_file));
253
254        Ok(mmap_file)
255    }
256
257    /// Clear the cache
258    pub fn clear(&self) {
259        self.cache.clear();
260    }
261
262    /// Get current cache size
263    pub fn len(&self) -> usize {
264        self.cache.len()
265    }
266
267    /// Check if cache is empty
268    pub fn is_empty(&self) -> bool {
269        self.cache.is_empty()
270    }
271}
272
273// ============================================================================
274// Platform-Specific Optimizations
275// ============================================================================
276
277/// Platform-specific optimization hints
278#[derive(Debug, Clone, Copy)]
279pub struct MmapConfig {
280    /// Use hugepages if available (Linux)
281    pub use_hugepages: bool,
282
283    /// Advise sequential access pattern
284    pub sequential_access: bool,
285
286    /// Advise random access pattern
287    pub random_access: bool,
288
289    /// Pre-populate the page tables (Linux)
290    pub populate: bool,
291}
292
293impl Default for MmapConfig {
294    fn default() -> Self {
295        MmapConfig {
296            use_hugepages: false,
297            sequential_access: true,
298            random_access: false,
299            populate: false,
300        }
301    }
302}
303
304impl MmapConfig {
305    /// Configuration optimized for sequential tensor streaming
306    pub fn sequential() -> Self {
307        MmapConfig {
308            use_hugepages: false,
309            sequential_access: true,
310            random_access: false,
311            populate: false,
312        }
313    }
314
315    /// Configuration optimized for random access (e.g., tensor slicing)
316    pub fn random() -> Self {
317        MmapConfig {
318            use_hugepages: false,
319            sequential_access: false,
320            random_access: true,
321            populate: false,
322        }
323    }
324
325    /// Configuration for large files with hugepage support
326    pub fn hugepages() -> Self {
327        MmapConfig {
328            use_hugepages: true,
329            sequential_access: false,
330            random_access: false,
331            populate: true,
332        }
333    }
334}
335
336// ============================================================================
337// Tests
338// ============================================================================
339
340#[cfg(test)]
341mod tests {
342    use super::*;
343    use std::io::Write;
344
345    fn create_test_file() -> (tempfile::NamedTempFile, Vec<u8>) {
346        let mut file = tempfile::NamedTempFile::new().unwrap();
347        let data: Vec<u8> = (0..1024).map(|i| (i % 256) as u8).collect();
348        file.write_all(&data).unwrap();
349        file.flush().unwrap();
350        (file, data)
351    }
352
353    #[test]
354    fn test_mmap_file_creation() {
355        let (file, _data) = create_test_file();
356        let mmap = MmapFile::new(file.path()).unwrap();
357        assert_eq!(mmap.size(), 1024);
358        assert!(!mmap.is_empty());
359    }
360
361    #[test]
362    fn test_mmap_file_not_found() {
363        let result = MmapFile::new("/nonexistent/file.bin");
364        assert!(result.is_err());
365        match result {
366            Err(MmapError::FileNotFound(_)) => {}
367            _ => panic!("Expected FileNotFound error"),
368        }
369    }
370
371    #[test]
372    fn test_mmap_bytes() {
373        let (file, data) = create_test_file();
374        let mmap = MmapFile::new(file.path()).unwrap();
375        let bytes = mmap.bytes();
376        assert_eq!(bytes.len(), 1024);
377        assert_eq!(&bytes[..], &data[..]);
378    }
379
380    #[test]
381    fn test_mmap_range() {
382        let (file, data) = create_test_file();
383        let mmap = MmapFile::new(file.path()).unwrap();
384
385        let range = mmap.range(10..50).unwrap();
386        assert_eq!(range.len(), 40);
387        assert_eq!(&range[..], &data[10..50]);
388    }
389
390    #[test]
391    fn test_mmap_range_invalid() {
392        let (file, _data) = create_test_file();
393        let mmap = MmapFile::new(file.path()).unwrap();
394
395        // Start > size
396        assert!(mmap.range(2000..2100).is_err());
397
398        // End > size
399        assert!(mmap.range(1000..2000).is_err());
400
401        // Start >= end
402        assert!(mmap.range(100..100).is_err());
403    }
404
405    #[test]
406    fn test_mmap_multi_range() {
407        let (file, data) = create_test_file();
408        let mmap = MmapFile::new(file.path()).unwrap();
409
410        let ranges = vec![0..10, 50..60, 100..120];
411        let results = mmap.multi_range(&ranges).unwrap();
412
413        assert_eq!(results.len(), 3);
414        assert_eq!(&results[0][..], &data[0..10]);
415        assert_eq!(&results[1][..], &data[50..60]);
416        assert_eq!(&results[2][..], &data[100..120]);
417    }
418
419    #[test]
420    fn test_mmap_clone() {
421        let (file, _data) = create_test_file();
422        let mmap1 = MmapFile::new(file.path()).unwrap();
423        let mmap2 = mmap1.clone();
424
425        assert_eq!(mmap1.size(), mmap2.size());
426        assert_eq!(mmap1.path(), mmap2.path());
427    }
428
429    #[test]
430    fn test_mmap_cache() {
431        let (file, _data) = create_test_file();
432        let cache = MmapCache::new(10);
433
434        // First access - creates mmap
435        let mmap1 = cache.get_or_create(file.path()).unwrap();
436        assert_eq!(cache.len(), 1);
437
438        // Second access - retrieves from cache
439        let mmap2 = cache.get_or_create(file.path()).unwrap();
440        assert_eq!(cache.len(), 1);
441
442        // Should be the same Arc
443        assert_eq!(mmap1.size(), mmap2.size());
444    }
445
446    #[test]
447    fn test_mmap_cache_clear() {
448        let (file, _data) = create_test_file();
449        let cache = MmapCache::new(10);
450
451        cache.get_or_create(file.path()).unwrap();
452        assert_eq!(cache.len(), 1);
453
454        cache.clear();
455        assert_eq!(cache.len(), 0);
456        assert!(cache.is_empty());
457    }
458
459    #[test]
460    fn test_mmap_config_presets() {
461        let sequential = MmapConfig::sequential();
462        assert!(sequential.sequential_access);
463        assert!(!sequential.random_access);
464
465        let random = MmapConfig::random();
466        assert!(!random.sequential_access);
467        assert!(random.random_access);
468
469        let hugepages = MmapConfig::hugepages();
470        assert!(hugepages.use_hugepages);
471        assert!(hugepages.populate);
472    }
473}