1use anyhow::{bail, Result};
10use lru::LruCache;
11use memmap2::Mmap;
12use oxirs_core::parallel::*;
13use parking_lot::RwLock;
14use std::collections::{HashMap, VecDeque};
15use std::num::NonZeroUsize;
16use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
17use std::sync::Arc;
18use std::time::Instant;
19use tracing::warn;
20
21const VECTOR_PAGE_SIZE: usize = 16384;
23
24const DEFAULT_MAX_PAGES: usize = 10000;
26
27#[cfg(target_os = "linux")]
29mod numa {
30 use libc::{c_ulong, c_void};
31
32 extern "C" {
33 fn numa_available() -> i32;
34 fn numa_max_node() -> i32;
35 fn numa_node_of_cpu(cpu: i32) -> i32;
36 fn numa_alloc_onnode(size: usize, node: i32) -> *mut c_void;
37 fn numa_free(ptr: *mut c_void, size: usize);
38 fn mbind(
39 addr: *mut c_void,
40 len: c_ulong,
41 mode: i32,
42 nodemask: *const c_ulong,
43 maxnode: c_ulong,
44 flags: u32,
45 ) -> i32;
46 }
47
48 pub const MPOL_BIND: i32 = 2;
49 pub const MPOL_INTERLEAVE: i32 = 3;
50
51 pub fn is_available() -> bool {
52 unsafe { numa_available() >= 0 }
53 }
54
55 pub fn max_node() -> i32 {
56 unsafe { numa_max_node() }
57 }
58
59 pub fn node_of_cpu(cpu: i32) -> i32 {
60 unsafe { numa_node_of_cpu(cpu) }
61 }
62}
63
64#[cfg(not(target_os = "linux"))]
65mod numa {
66 pub fn is_available() -> bool {
67 false
68 }
69 pub fn max_node() -> i32 {
70 0
71 }
72 pub fn node_of_cpu(_cpu: i32) -> i32 {
73 0
74 }
75}
76
77#[derive(Debug, Clone)]
79struct AccessPattern {
80 page_id: usize,
81 access_time: Instant,
82 access_count: usize,
83}
84
85#[derive(Debug)]
87pub struct PageCacheEntry {
88 data: Vec<u8>,
89 page_id: usize,
90 last_access: Instant,
91 access_count: AtomicUsize,
92 dirty: bool,
93 numa_node: i32,
94}
95
96impl PageCacheEntry {
97 pub fn data(&self) -> &[u8] {
99 &self.data
100 }
101
102 pub fn numa_node(&self) -> i32 {
104 self.numa_node
105 }
106}
107
108#[derive(Debug, Clone, Copy)]
110pub enum EvictionPolicy {
111 LRU, LFU, FIFO, Clock, ARC, }
117
118#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
120pub enum MemoryPressure {
121 Low,
122 Medium,
123 High,
124 Critical,
125}
126
127pub struct AdvancedMemoryMap {
129 mmap: Option<Mmap>,
131
132 file_path: Option<std::path::PathBuf>,
134
135 page_cache: Arc<RwLock<LruCache<usize, Arc<PageCacheEntry>>>>,
137
138 access_patterns: Arc<RwLock<VecDeque<AccessPattern>>>,
140
141 page_frequency: Arc<RwLock<HashMap<usize, usize>>>,
143
144 eviction_policy: EvictionPolicy,
146
147 total_memory: AtomicUsize,
149 cache_hits: AtomicU64,
150 cache_misses: AtomicU64,
151
152 numa_enabled: bool,
154 numa_nodes: Vec<i32>,
155
156 memory_pressure: Arc<RwLock<MemoryPressure>>,
158
159 max_pages: usize,
161 page_size: usize,
162 prefetch_distance: usize,
163}
164
165impl AdvancedMemoryMap {
166 pub fn new(mmap: Option<Mmap>, max_pages: usize) -> Self {
168 let numa_enabled = numa::is_available();
169 let numa_nodes = if numa_enabled {
170 (0..=numa::max_node()).collect()
171 } else {
172 vec![0]
173 };
174
175 let cache_size = NonZeroUsize::new(max_pages)
176 .unwrap_or(NonZeroUsize::new(1).expect("constant 1 is non-zero"));
177
178 Self {
179 mmap,
180 file_path: None,
181 page_cache: Arc::new(RwLock::new(LruCache::new(cache_size))),
182 access_patterns: Arc::new(RwLock::new(VecDeque::with_capacity(1000))),
183 page_frequency: Arc::new(RwLock::new(HashMap::new())),
184 eviction_policy: EvictionPolicy::ARC,
185 total_memory: AtomicUsize::new(0),
186 cache_hits: AtomicU64::new(0),
187 cache_misses: AtomicU64::new(0),
188 numa_enabled,
189 numa_nodes,
190 memory_pressure: Arc::new(RwLock::new(MemoryPressure::Low)),
191 max_pages,
192 page_size: VECTOR_PAGE_SIZE,
193 prefetch_distance: 3,
194 }
195 }
196
197 pub fn new_with_path(
199 mmap: Option<Mmap>,
200 max_pages: usize,
201 file_path: Option<std::path::PathBuf>,
202 ) -> Self {
203 let mut s = Self::new(mmap, max_pages);
204 s.file_path = file_path;
205 s
206 }
207
208 pub fn get_page(&self, page_id: usize) -> Result<Arc<PageCacheEntry>> {
210 {
212 let mut cache = self.page_cache.write();
213 if let Some(entry) = cache.get(&page_id) {
214 self.cache_hits.fetch_add(1, Ordering::Relaxed);
215 entry.access_count.fetch_add(1, Ordering::Relaxed);
216 self.record_access(page_id);
217 return Ok(Arc::clone(entry));
218 }
219 }
220
221 self.cache_misses.fetch_add(1, Ordering::Relaxed);
223 self.load_page(page_id)
224 }
225
226 fn load_page(&self, page_id: usize) -> Result<Arc<PageCacheEntry>> {
228 let mmap = self
229 .mmap
230 .as_ref()
231 .ok_or_else(|| anyhow::anyhow!("No memory mapping available"))?;
232
233 let start = page_id * self.page_size;
234 let end = (start + self.page_size).min(mmap.len());
235
236 if start >= mmap.len() {
237 bail!("Page {} out of bounds", page_id);
238 }
239
240 let page_data = mmap[start..end].to_vec();
242
243 let numa_node = if self.numa_enabled {
245 let cpu = sched_getcpu();
246 numa::node_of_cpu(cpu)
247 } else {
248 0
249 };
250
251 let entry = Arc::new(PageCacheEntry {
252 data: page_data,
253 page_id,
254 last_access: Instant::now(),
255 access_count: AtomicUsize::new(1),
256 dirty: false,
257 numa_node,
258 });
259
260 self.check_memory_pressure();
262 if *self.memory_pressure.read() >= MemoryPressure::High {
263 self.evict_pages(1)?;
264 }
265
266 {
268 let mut cache = self.page_cache.write();
269 cache.put(page_id, Arc::clone(&entry));
270 }
271
272 self.total_memory
273 .fetch_add(entry.data.len(), Ordering::Relaxed);
274 self.record_access(page_id);
275
276 self.prefetch_pages(page_id);
278
279 Ok(entry)
280 }
281
282 fn record_access(&self, page_id: usize) {
284 let mut patterns = self.access_patterns.write();
285 patterns.push_back(AccessPattern {
286 page_id,
287 access_time: Instant::now(),
288 access_count: 1,
289 });
290
291 while patterns.len() > 1000 {
293 patterns.pop_front();
294 }
295
296 let mut freq = self.page_frequency.write();
298 *freq.entry(page_id).or_insert(0) += 1;
299 }
300
301 fn prefetch_pages(&self, current_page: usize) {
303 let patterns = self.access_patterns.read();
304 let freq = self.page_frequency.read();
305
306 let recent_patterns: Vec<_> = patterns.iter().rev().take(10).collect();
308
309 let is_sequential = recent_patterns
311 .windows(2)
312 .all(|w| w[0].page_id > 0 && w[0].page_id == w[1].page_id + 1);
313
314 let stride = if recent_patterns.len() >= 3 {
316 let diff1 = recent_patterns[0]
317 .page_id
318 .saturating_sub(recent_patterns[1].page_id);
319 let diff2 = recent_patterns[1]
320 .page_id
321 .saturating_sub(recent_patterns[2].page_id);
322 if diff1 == diff2 && diff1 > 0 && diff1 <= 10 {
323 Some(diff1)
324 } else {
325 None
326 }
327 } else {
328 None
329 };
330
331 if is_sequential {
333 for i in 1..=(self.prefetch_distance * 2) {
335 let prefetch_page = current_page + i;
336 self.async_prefetch(prefetch_page);
337 }
338 } else if let Some(stride) = stride {
339 for i in 1..=self.prefetch_distance {
341 let prefetch_page = current_page + (i * stride);
342 self.async_prefetch(prefetch_page);
343 }
344 } else {
345 for i in 1..=self.prefetch_distance {
347 let prefetch_page = current_page + i;
348
349 let frequency = *freq.get(&prefetch_page).unwrap_or(&0);
351 if frequency > 0 {
352 self.async_prefetch(prefetch_page);
353 }
354 }
355 }
356
357 let nearby_range = current_page.saturating_sub(3)..=(current_page + 3);
359 for page_id in nearby_range {
360 let frequency = *freq.get(&page_id).unwrap_or(&0);
361 if frequency > 2 && page_id != current_page {
362 self.async_prefetch(page_id);
363 }
364 }
365 }
366
367 pub fn async_prefetch(&self, page_id: usize) {
369 {
371 let cache = self.page_cache.read();
372 if cache.contains(&page_id) {
373 return;
374 }
375 }
376
377 if *self.memory_pressure.read() >= MemoryPressure::High {
379 return;
380 }
381
382 let self_clone = self.clone_ref();
383 spawn(move || {
384 let _ = self_clone.get_page(page_id);
385 });
386 }
387
388 fn check_memory_pressure(&self) {
390 let total_memory = self.total_memory.load(Ordering::Relaxed);
391 let max_memory = self.max_pages * self.page_size;
392
393 let pressure = if total_memory < max_memory / 2 {
394 MemoryPressure::Low
395 } else if total_memory < max_memory * 3 / 4 {
396 MemoryPressure::Medium
397 } else if total_memory < max_memory * 9 / 10 {
398 MemoryPressure::High
399 } else {
400 MemoryPressure::Critical
401 };
402
403 *self.memory_pressure.write() = pressure;
404 }
405
406 fn evict_pages(&self, num_pages: usize) -> Result<()> {
408 match self.eviction_policy {
409 EvictionPolicy::LRU => self.evict_lru(num_pages),
410 EvictionPolicy::LFU => self.evict_lfu(num_pages),
411 EvictionPolicy::FIFO => self.evict_fifo(num_pages),
412 EvictionPolicy::Clock => self.evict_clock(num_pages),
413 EvictionPolicy::ARC => self.evict_arc(num_pages),
414 }
415 }
416
417 fn evict_lru(&self, num_pages: usize) -> Result<()> {
419 let mut cache = self.page_cache.write();
420
421 for _ in 0..num_pages {
423 if let Some((_, entry)) = cache.pop_lru() {
424 self.total_memory
425 .fetch_sub(entry.data.len(), Ordering::Relaxed);
426
427 if entry.dirty {
429 if let Err(e) = self.write_back_page(entry.page_id, &entry.data) {
430 warn!("Failed to write back page {}: {}", entry.page_id, e);
431 }
432 }
433 }
434 }
435
436 Ok(())
437 }
438
439 fn evict_lfu(&self, num_pages: usize) -> Result<()> {
441 let cache = self.page_cache.read();
442 let freq = self.page_frequency.read();
443
444 let mut pages_by_freq: Vec<(usize, usize)> = cache
446 .iter()
447 .map(|(page_id, _)| (*page_id, *freq.get(page_id).unwrap_or(&0)))
448 .collect();
449 pages_by_freq.sort_by_key(|(_, freq)| *freq);
450
451 drop(cache);
453 drop(freq);
454
455 let mut cache = self.page_cache.write();
456 for (page_id, _) in pages_by_freq.iter().take(num_pages) {
457 if let Some(entry) = cache.pop(page_id) {
458 self.total_memory
459 .fetch_sub(entry.data.len(), Ordering::Relaxed);
460 if entry.dirty {
461 if let Err(e) = self.write_back_page(entry.page_id, &entry.data) {
462 warn!("Failed to write back dirty page {}: {}", entry.page_id, e);
463 }
464 }
465 }
466 }
467
468 Ok(())
469 }
470
471 fn evict_fifo(&self, num_pages: usize) -> Result<()> {
473 self.evict_lru(num_pages)
474 }
475
476 fn evict_clock(&self, num_pages: usize) -> Result<()> {
478 self.evict_lru(num_pages)
479 }
480
481 fn evict_arc(&self, num_pages: usize) -> Result<()> {
483 let cache = self.page_cache.read();
485 let freq = self.page_frequency.read();
486
487 let now = Instant::now();
489 let mut scored_pages: Vec<(usize, f64)> = cache
490 .iter()
491 .map(|(page_id, entry)| {
492 let recency_score =
493 1.0 / (now.duration_since(entry.last_access).as_secs_f64() + 1.0);
494 let frequency_score = *freq.get(page_id).unwrap_or(&0) as f64;
495 let combined_score = recency_score * 0.5 + frequency_score * 0.5;
496 (*page_id, combined_score)
497 })
498 .collect();
499
500 scored_pages.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
501
502 drop(cache);
503 drop(freq);
504
505 let mut cache = self.page_cache.write();
506 for (page_id, _) in scored_pages.iter().take(num_pages) {
507 if let Some(entry) = cache.pop(page_id) {
508 self.total_memory
509 .fetch_sub(entry.data.len(), Ordering::Relaxed);
510 if entry.dirty {
511 if let Err(e) = self.write_back_page(entry.page_id, &entry.data) {
512 warn!("Failed to write back dirty page {}: {}", entry.page_id, e);
513 }
514 }
515 }
516 }
517
518 Ok(())
519 }
520
521 pub fn stats(&self) -> MemoryMapStats {
523 let cache = self.page_cache.read();
524
525 MemoryMapStats {
526 total_pages: cache.len(),
527 total_memory: self.total_memory.load(Ordering::Relaxed),
528 cache_hits: self.cache_hits.load(Ordering::Relaxed),
529 cache_misses: self.cache_misses.load(Ordering::Relaxed),
530 hit_rate: self.calculate_hit_rate(),
531 memory_pressure: *self.memory_pressure.read(),
532 numa_enabled: self.numa_enabled,
533 }
534 }
535
536 fn calculate_hit_rate(&self) -> f64 {
537 let hits = self.cache_hits.load(Ordering::Relaxed) as f64;
538 let misses = self.cache_misses.load(Ordering::Relaxed) as f64;
539 let total = hits + misses;
540 if total > 0.0 {
541 hits / total
542 } else {
543 0.0
544 }
545 }
546
547 fn clone_ref(&self) -> Self {
548 Self {
549 mmap: None, file_path: self.file_path.clone(),
551 page_cache: Arc::clone(&self.page_cache),
552 access_patterns: Arc::clone(&self.access_patterns),
553 page_frequency: Arc::clone(&self.page_frequency),
554 eviction_policy: self.eviction_policy,
555 total_memory: AtomicUsize::new(0),
556 cache_hits: AtomicU64::new(0),
557 cache_misses: AtomicU64::new(0),
558 numa_enabled: self.numa_enabled,
559 numa_nodes: self.numa_nodes.clone(),
560 memory_pressure: Arc::clone(&self.memory_pressure),
561 max_pages: self.max_pages,
562 page_size: self.page_size,
563 prefetch_distance: self.prefetch_distance,
564 }
565 }
566
567 fn write_back_page(&self, page_id: usize, data: &[u8]) -> Result<()> {
569 use std::io::{Seek, SeekFrom, Write};
570 let path = match &self.file_path {
571 Some(p) => p,
572 None => return Ok(()), };
574 let mut file = std::fs::OpenOptions::new()
575 .write(true)
576 .open(path)
577 .map_err(|e| anyhow::anyhow!("Failed to open file for write-back: {}", e))?;
578 let offset = (page_id * self.page_size) as u64;
579 file.seek(SeekFrom::Start(offset))
580 .map_err(|e| anyhow::anyhow!("Failed to seek to page {}: {}", page_id, e))?;
581 file.write_all(data)
582 .map_err(|e| anyhow::anyhow!("Failed to write page {}: {}", page_id, e))?;
583 Ok(())
584 }
585
586 pub fn flush_dirty_pages(&self) -> Result<()> {
588 if self.file_path.is_none() {
589 return Ok(());
590 }
591 let cache = self.page_cache.read();
592 for (_, entry) in cache.iter() {
593 if entry.dirty {
594 self.write_back_page(entry.page_id, &entry.data)?;
595 }
596 }
597 Ok(())
598 }
599}
600
601#[derive(Debug, Clone)]
603pub struct MemoryMapStats {
604 pub total_pages: usize,
605 pub total_memory: usize,
606 pub cache_hits: u64,
607 pub cache_misses: u64,
608 pub hit_rate: f64,
609 pub memory_pressure: MemoryPressure,
610 pub numa_enabled: bool,
611}
612
613#[cfg(target_os = "linux")]
615fn sched_getcpu() -> i32 {
616 unsafe { libc::sched_getcpu() }
617}
618
619#[cfg(not(target_os = "linux"))]
620fn sched_getcpu() -> i32 {
621 0
622}
623
624pub struct NumaVectorAllocator {
626 numa_nodes: Vec<i32>,
627 current_node: AtomicUsize,
628}
629
630impl Default for NumaVectorAllocator {
631 fn default() -> Self {
632 Self::new()
633 }
634}
635
636impl NumaVectorAllocator {
637 pub fn new() -> Self {
638 let numa_nodes = if numa::is_available() {
639 (0..=numa::max_node()).collect()
640 } else {
641 vec![0]
642 };
643
644 Self {
645 numa_nodes,
646 current_node: AtomicUsize::new(0),
647 }
648 }
649
650 pub fn allocate_on_node(&self, size: usize, node: Option<i32>) -> Vec<u8> {
652 if !numa::is_available() {
653 return vec![0u8; size];
654 }
655
656 let _target_node = node.unwrap_or_else(|| {
657 let idx = self.current_node.fetch_add(1, Ordering::Relaxed) % self.numa_nodes.len();
659 self.numa_nodes[idx]
660 });
661
662 vec![0u8; size]
665 }
666
667 pub fn allocate_vector_on_node(&self, dimensions: usize, node: Option<i32>) -> Vec<f32> {
669 if !numa::is_available() {
670 let mut vec = Vec::with_capacity(dimensions);
672 vec.resize(dimensions, 0.0f32);
673 return vec;
674 }
675
676 let _target_node = node.unwrap_or_else(|| {
677 self.preferred_node()
679 });
680
681 let mut vec = Vec::with_capacity(dimensions);
683 vec.resize(dimensions, 0.0f32);
684
685 vec
689 }
690
691 pub fn preferred_node(&self) -> i32 {
693 if numa::is_available() {
694 numa::node_of_cpu(sched_getcpu())
695 } else {
696 0
697 }
698 }
699}
700
701#[cfg(test)]
702mod tests {
703 use super::*;
704
705 #[test]
706 fn test_memory_pressure() {
707 let mmap = AdvancedMemoryMap::new(None, 100);
708
709 assert_eq!(*mmap.memory_pressure.read(), MemoryPressure::Low);
710
711 mmap.total_memory
713 .store(50 * VECTOR_PAGE_SIZE, Ordering::Relaxed);
714 mmap.check_memory_pressure();
715 assert_eq!(*mmap.memory_pressure.read(), MemoryPressure::Medium);
716
717 mmap.total_memory
718 .store(90 * VECTOR_PAGE_SIZE, Ordering::Relaxed);
719 mmap.check_memory_pressure();
720 assert_eq!(*mmap.memory_pressure.read(), MemoryPressure::Critical);
721 }
722
723 #[test]
724 fn test_cache_stats() {
725 let mmap = AdvancedMemoryMap::new(None, 100);
726
727 mmap.cache_hits.store(75, Ordering::Relaxed);
728 mmap.cache_misses.store(25, Ordering::Relaxed);
729
730 let stats = mmap.stats();
731 assert_eq!(stats.cache_hits, 75);
732 assert_eq!(stats.cache_misses, 25);
733 assert_eq!(stats.hit_rate, 0.75);
734 }
735}