Skip to main content

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 =
347            tempfile::NamedTempFile::new().expect("test: temp file creation should succeed");
348        let data: Vec<u8> = (0..1024).map(|i| (i % 256) as u8).collect();
349        file.write_all(&data)
350            .expect("test: write to temp file should succeed");
351        file.flush()
352            .expect("test: flush to temp file should succeed");
353        (file, data)
354    }
355
356    #[test]
357    fn test_mmap_file_creation() {
358        let (file, _data) = create_test_file();
359        let mmap = MmapFile::new(file.path()).expect("test: mmap creation should succeed");
360        assert_eq!(mmap.size(), 1024);
361        assert!(!mmap.is_empty());
362    }
363
364    #[test]
365    fn test_mmap_file_not_found() {
366        let result = MmapFile::new("/nonexistent/file.bin");
367        assert!(result.is_err());
368        match result {
369            Err(MmapError::FileNotFound(_)) => {}
370            _ => panic!("Expected FileNotFound error"),
371        }
372    }
373
374    #[test]
375    fn test_mmap_bytes() {
376        let (file, data) = create_test_file();
377        let mmap = MmapFile::new(file.path()).expect("test: mmap creation should succeed");
378        let bytes = mmap.bytes();
379        assert_eq!(bytes.len(), 1024);
380        assert_eq!(&bytes[..], &data[..]);
381    }
382
383    #[test]
384    fn test_mmap_range() {
385        let (file, data) = create_test_file();
386        let mmap = MmapFile::new(file.path()).expect("test: mmap creation should succeed");
387
388        let range = mmap
389            .range(10..50)
390            .expect("test: range slice should succeed");
391        assert_eq!(range.len(), 40);
392        assert_eq!(&range[..], &data[10..50]);
393    }
394
395    #[test]
396    fn test_mmap_range_invalid() {
397        let (file, _data) = create_test_file();
398        let mmap = MmapFile::new(file.path()).expect("test: mmap creation should succeed");
399
400        // Start > size
401        assert!(mmap.range(2000..2100).is_err());
402
403        // End > size
404        assert!(mmap.range(1000..2000).is_err());
405
406        // Start >= end
407        assert!(mmap.range(100..100).is_err());
408    }
409
410    #[test]
411    fn test_mmap_multi_range() {
412        let (file, data) = create_test_file();
413        let mmap = MmapFile::new(file.path()).expect("test: mmap creation should succeed");
414
415        let ranges = vec![0..10, 50..60, 100..120];
416        let results = mmap
417            .multi_range(&ranges)
418            .expect("test: multi_range should succeed");
419
420        assert_eq!(results.len(), 3);
421        assert_eq!(&results[0][..], &data[0..10]);
422        assert_eq!(&results[1][..], &data[50..60]);
423        assert_eq!(&results[2][..], &data[100..120]);
424    }
425
426    #[test]
427    fn test_mmap_clone() {
428        let (file, _data) = create_test_file();
429        let mmap1 = MmapFile::new(file.path()).expect("test: mmap creation should succeed");
430        let mmap2 = mmap1.clone();
431
432        assert_eq!(mmap1.size(), mmap2.size());
433        assert_eq!(mmap1.path(), mmap2.path());
434    }
435
436    #[test]
437    fn test_mmap_cache() {
438        let (file, _data) = create_test_file();
439        let cache = MmapCache::new(10);
440
441        // First access - creates mmap
442        let mmap1 = cache
443            .get_or_create(file.path())
444            .expect("test: cache get_or_create should succeed");
445        assert_eq!(cache.len(), 1);
446
447        // Second access - retrieves from cache
448        let mmap2 = cache
449            .get_or_create(file.path())
450            .expect("test: cache get_or_create should succeed");
451        assert_eq!(cache.len(), 1);
452
453        // Should be the same Arc
454        assert_eq!(mmap1.size(), mmap2.size());
455    }
456
457    #[test]
458    fn test_mmap_cache_clear() {
459        let (file, _data) = create_test_file();
460        let cache = MmapCache::new(10);
461
462        cache
463            .get_or_create(file.path())
464            .expect("test: cache get_or_create should succeed");
465        assert_eq!(cache.len(), 1);
466
467        cache.clear();
468        assert_eq!(cache.len(), 0);
469        assert!(cache.is_empty());
470    }
471
472    #[test]
473    fn test_mmap_config_presets() {
474        let sequential = MmapConfig::sequential();
475        assert!(sequential.sequential_access);
476        assert!(!sequential.random_access);
477
478        let random = MmapConfig::random();
479        assert!(!random.sequential_access);
480        assert!(random.random_access);
481
482        let hugepages = MmapConfig::hugepages();
483        assert!(hugepages.use_hugepages);
484        assert!(hugepages.populate);
485    }
486}