velesdb_core/storage/mmap_capacity.rs
1//! `MmapStorage` capacity management and compaction.
2//!
3//! Extracted from `mmap.rs` to reduce NLOC below the 500 threshold.
4
5use super::compaction::CompactionContext;
6use super::mmap::MmapStorage;
7
8use memmap2::MmapMut;
9use std::fs::OpenOptions;
10use std::io;
11use std::time::Instant;
12
13impl MmapStorage {
14 /// Ensures the memory map is large enough to hold data at `offset`.
15 ///
16 /// # P2 Optimization
17 ///
18 /// Uses aggressive pre-allocation to minimize blocking:
19 /// - Exponential growth (2x) for amortized O(1)
20 /// - 64MB minimum growth to reduce resize frequency
21 pub(crate) fn ensure_capacity(&mut self, required_len: usize) -> io::Result<()> {
22 let start = Instant::now();
23 let mut did_resize = false;
24 let mut bytes_resized = 0u64;
25
26 let mut mmap = self.mmap.write();
27 if mmap.len() < required_len {
28 mmap.flush()?;
29
30 let current_len = mmap.len() as u64;
31 let required_u64 = required_len as u64;
32
33 let doubled = current_len.saturating_mul(Self::GROWTH_FACTOR);
34 let with_headroom = required_u64.saturating_add(Self::MIN_GROWTH);
35 let min_growth = current_len.saturating_add(Self::MIN_GROWTH);
36
37 let new_len = doubled.max(with_headroom).max(min_growth).max(required_u64);
38
39 self.data_file.set_len(new_len)?;
40
41 // SAFETY: data_file has been resized with set_len(new_len) above,
42 // ensuring the new mapping range is fully allocated.
43 // - Condition 1: File was resized to new_len before remapping.
44 // - Condition 2: Old mmap is dropped when we assign the new one.
45 // - Condition 3: File remains open with read+write permissions.
46 // SAFETY: Memory mapping requires unsafe; resizing ensures mapping doesn't exceed file bounds.
47 *mmap = unsafe { MmapMut::map_mut(&self.data_file)? };
48 self.remap_epoch
49 .fetch_add(1, std::sync::atomic::Ordering::Release);
50
51 did_resize = true;
52 bytes_resized = new_len.saturating_sub(current_len);
53 }
54
55 self.metrics
56 .record_ensure_capacity(start.elapsed(), did_resize, bytes_resized);
57
58 Ok(())
59 }
60
61 /// Pre-allocates storage capacity for a known number of vectors.
62 ///
63 /// # Errors
64 ///
65 /// Returns an error if file operations fail.
66 pub fn reserve_capacity(&mut self, vector_count: usize) -> io::Result<()> {
67 let vector_size = self.dimension * std::mem::size_of::<f32>();
68 let required_len = vector_count.saturating_mul(vector_size);
69 let with_headroom = required_len.saturating_add(required_len / 10);
70 self.ensure_capacity(with_headroom)
71 }
72
73 /// Compacts the storage by rewriting only active vectors.
74 ///
75 /// # Returns
76 ///
77 /// The number of bytes reclaimed.
78 ///
79 /// # Errors
80 ///
81 /// Returns an error if file operations fail.
82 pub fn compact(&mut self) -> io::Result<usize> {
83 let ctx = CompactionContext {
84 path: &self.path,
85 dimension: self.dimension,
86 index: &self.index,
87 mmap: &self.mmap,
88 next_offset: &self.next_offset,
89 wal: &self.wal,
90 initial_size: Self::INITIAL_SIZE,
91 };
92
93 let bytes_reclaimed = ctx.compact()?;
94
95 if bytes_reclaimed > 0 {
96 let data_path = self.path.join("vectors.dat");
97 self.data_file = OpenOptions::new().read(true).write(true).open(&data_path)?;
98 // No flush_full() here: `commit_compaction` already synced the
99 // compacted data file before the swap, promoted an identical
100 // fsynced vectors.idx and truncated the WAL. Rewriting
101 // vectors.idx at this point is redundant (`&mut self` excludes
102 // concurrent mutation) and used to reopen the exact crash window
103 // the staged commit closed — a torn index next to an
104 // already-empty WAL is unrecoverable (audit 2026-06, finding 3).
105 }
106
107 Ok(bytes_reclaimed)
108 }
109
110 /// Returns the fragmentation ratio (0.0 = no fragmentation, 1.0 = 100% fragmented).
111 #[must_use]
112 pub fn fragmentation_ratio(&self) -> f64 {
113 let ctx = CompactionContext {
114 path: &self.path,
115 dimension: self.dimension,
116 index: &self.index,
117 mmap: &self.mmap,
118 next_offset: &self.next_offset,
119 wal: &self.wal,
120 initial_size: Self::INITIAL_SIZE,
121 };
122
123 ctx.fragmentation_ratio()
124 }
125}