1use std::sync::{Arc, OnceLock};
14
15use crate::directories::OwnedBytes;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum PinMode {
20 Mlock,
24 Copy,
27}
28
29#[derive(Debug, Clone, Copy)]
31pub struct PinPolicy {
32 pub budget_bytes: u64,
34 pub mode: PinMode,
35}
36
37impl PinPolicy {
38 pub const fn disabled() -> Self {
39 Self {
40 budget_bytes: 0,
41 mode: PinMode::Mlock,
42 }
43 }
44
45 pub fn is_enabled(&self) -> bool {
46 self.budget_bytes > 0
47 }
48
49 pub fn from_env() -> Self {
57 let budget_bytes = match std::env::var("SUMMA_PIN_METADATA_BUDGET_MB") {
58 Ok(value) => match value
59 .parse::<u64>()
60 .ok()
61 .and_then(|mb| mb.checked_mul(1024 * 1024))
62 {
63 Some(bytes) => bytes,
64 None => {
65 log::warn!(
66 "SUMMA_PIN_METADATA_BUDGET_MB '{}' is invalid or too large; \
67 using budget 0 (pinning disabled); set a non-negative MiB value \
68 no larger than {}",
69 value,
70 u64::MAX / (1024 * 1024),
71 );
72 0
73 }
74 },
75 Err(std::env::VarError::NotPresent) => 0,
76 Err(error) => {
77 log::warn!(
78 "SUMMA_PIN_METADATA_BUDGET_MB cannot be read: {}; pinning disabled",
79 error
80 );
81 0
82 }
83 };
84 let mode = match std::env::var("SUMMA_PIN_MODE").as_deref() {
85 Ok("copy") => PinMode::Copy,
86 Ok("mlock") | Err(_) => PinMode::Mlock,
87 Ok(other) => {
88 log::warn!("SUMMA_PIN_MODE '{}' unknown; using mlock", other);
89 PinMode::Mlock
90 }
91 };
92 Self { budget_bytes, mode }
93 }
94}
95
96static PIN_POLICY: OnceLock<PinPolicy> = OnceLock::new();
97
98pub const UNPINNED_METADATA_WARN_THRESHOLD_BYTES: u64 = 16 * 1024 * 1024;
102
103static WARNED_PINNING_DISABLED: std::sync::atomic::AtomicBool =
104 std::sync::atomic::AtomicBool::new(false);
105
106pub(crate) fn warn_if_pinning_disabled(index_label: &str, segment_id: u128, intended_bytes: u64) {
112 if intended_bytes < UNPINNED_METADATA_WARN_THRESHOLD_BYTES {
113 return;
114 }
115 if WARNED_PINNING_DISABLED.swap(true, std::sync::atomic::Ordering::Relaxed) {
116 return;
117 }
118 log::warn!(
119 "[pin] index={} segment {:016x}: hot-metadata pinning is disabled (budget 0) but this \
120 segment has {} of mmap-backed per-query metadata; set SUMMA_PIN_METADATA_BUDGET_MB \
121 (and SUMMA_PIN_MODE=mlock|copy) to keep it resident under memory pressure \
122 (reported once per process)",
123 index_label,
124 segment_id,
125 crate::format_bytes(intended_bytes),
126 );
127}
128
129pub fn set_pin_policy(policy: PinPolicy) -> bool {
133 let ok = PIN_POLICY.set(policy).is_ok();
134 if !ok {
135 log::warn!("pin policy already initialized; set_pin_policy ignored");
136 }
137 ok
138}
139
140pub fn pin_policy() -> &'static PinPolicy {
142 PIN_POLICY.get_or_init(PinPolicy::from_env)
143}
144
145#[derive(Debug, Default, Clone, Copy)]
147pub struct PinReport {
148 pub intended_bytes: u64,
150 pub pinned_bytes: u64,
152 pub skipped_budget_bytes: u64,
154 pub failed_bytes: u64,
156 pub heap_copy_bytes: u64,
159}
160
161struct HeapPinGuard {
166 page_start: *mut libc::c_void,
167 page_len: usize,
168}
169
170unsafe impl Send for HeapPinGuard {}
173unsafe impl Sync for HeapPinGuard {}
174
175impl Drop for HeapPinGuard {
176 fn drop(&mut self) {
177 if unsafe { libc::munlock(self.page_start, self.page_len) } != 0 {
178 log::warn!(
179 "[pin] munlock failed for {} of ANN heap: {}",
180 crate::format_bytes(self.page_len as u64),
181 std::io::Error::last_os_error()
182 );
183 }
184 }
185}
186
187#[derive(Default)]
190pub(crate) struct HeapPinSet {
191 guards: Vec<HeapPinGuard>,
192 owners: Vec<Arc<dyn std::any::Any + Send + Sync>>,
195 report: PinReport,
196}
197
198impl HeapPinSet {
199 pub(crate) fn report(&self) -> PinReport {
200 self.report
201 }
202
203 pub(crate) fn retain_owner<T: std::any::Any + Send + Sync>(&mut self, owner: Arc<T>) {
204 self.owners.push(owner);
205 }
206
207 pub(crate) fn pin_slice<T>(
212 &mut self,
213 slice: &[T],
214 label: &str,
215 mode: PinMode,
216 remaining: &mut u64,
217 ) {
218 let len = std::mem::size_of_val(slice);
219 if len == 0 {
220 return;
221 }
222 let Ok(len_u64) = u64::try_from(len) else {
223 self.report.failed_bytes = u64::MAX;
224 log::warn!("[pin] ANN region {label} is too large to account");
225 return;
226 };
227 self.report.intended_bytes = self.report.intended_bytes.saturating_add(len_u64);
228 if len_u64 > *remaining {
229 self.report.skipped_budget_bytes =
230 self.report.skipped_budget_bytes.saturating_add(len_u64);
231 log::debug!(
232 "[pin] ANN budget exhausted: skipping {} ({}, {} remaining)",
233 label,
234 crate::format_bytes(len_u64),
235 crate::format_bytes(*remaining)
236 );
237 return;
238 }
239
240 if mode == PinMode::Copy {
241 *remaining -= len_u64;
242 self.report.pinned_bytes = self.report.pinned_bytes.saturating_add(len_u64);
243 return;
244 }
245
246 let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
247 let page_size = usize::try_from(page_size).ok().filter(|&size| size > 0);
248 let Some(page_size) = page_size else {
249 self.report.failed_bytes = self.report.failed_bytes.saturating_add(len_u64);
250 log::warn!("[pin] cannot determine page size while locking {label}");
251 return;
252 };
253 let address = slice.as_ptr() as usize;
254 let page_start = address / page_size * page_size;
255 let Some(end) = address.checked_add(len) else {
256 self.report.failed_bytes = self.report.failed_bytes.saturating_add(len_u64);
257 log::warn!("[pin] ANN region address overflow while locking {label}");
258 return;
259 };
260 let Some(rounded_end) = end
261 .checked_add(page_size - 1)
262 .map(|value| value / page_size * page_size)
263 else {
264 self.report.failed_bytes = self.report.failed_bytes.saturating_add(len_u64);
265 log::warn!("[pin] ANN region page range overflow while locking {label}");
266 return;
267 };
268 let page_len = rounded_end - page_start;
269 let page_start = page_start as *mut libc::c_void;
270 if unsafe { libc::mlock(page_start.cast_const(), page_len) } == 0 {
271 self.guards.push(HeapPinGuard {
272 page_start,
273 page_len,
274 });
275 *remaining -= len_u64;
276 self.report.pinned_bytes = self.report.pinned_bytes.saturating_add(len_u64);
277 } else {
278 self.report.failed_bytes = self.report.failed_bytes.saturating_add(len_u64);
279 log::warn!(
280 "[pin] mlock failed for ANN {} ({}): {} — check RLIMIT_MEMLOCK/CAP_IPC_LOCK; continuing unpinned",
281 label,
282 crate::format_bytes(len_u64),
283 std::io::Error::last_os_error()
284 );
285 }
286 }
287}
288
289pub(crate) fn pin_section(
296 bytes: &mut OwnedBytes,
297 label: &str,
298 mode: PinMode,
299 remaining: &mut u64,
300 report: &mut PinReport,
301) {
302 if !bytes.is_mmap() || bytes.is_empty() {
303 return;
304 }
305 let len = bytes.len() as u64;
306 report.intended_bytes += len;
307
308 if len > *remaining {
309 report.skipped_budget_bytes += len;
310 log::debug!(
311 "[pin] budget exhausted: skipping {} ({}, {} remaining)",
312 label,
313 crate::format_bytes(len),
314 crate::format_bytes(*remaining)
315 );
316 return;
317 }
318
319 match mode {
320 PinMode::Mlock => {
321 if bytes.mlock() {
322 *remaining -= len;
323 report.pinned_bytes += len;
324 } else {
325 report.failed_bytes += len;
326 log::warn!(
327 "[pin] mlock failed for {} ({}) — check RLIMIT_MEMLOCK; \
328 continuing unpinned",
329 label,
330 crate::format_bytes(len)
331 );
332 }
333 }
334 PinMode::Copy => {
335 *bytes = copy_section(bytes);
336 *remaining -= len;
337 report.pinned_bytes += len;
338 report.heap_copy_bytes += len;
339 }
340 }
341}
342
343fn copy_section(bytes: &OwnedBytes) -> OwnedBytes {
346 #[cfg(target_os = "linux")]
347 {
348 const CHUNK: usize = 128 * 1024;
349 let mut copied = Vec::with_capacity(bytes.len());
350 let mut prefetched = 0;
351 for (i, chunk) in bytes.chunks(CHUNK).enumerate() {
352 let end = (i * CHUNK).saturating_add(2 * CHUNK).min(bytes.len());
353 while prefetched < end {
354 let next = prefetched.saturating_add(CHUNK).min(end);
355 bytes.madvise_range(prefetched..next, libc::MADV_WILLNEED);
356 prefetched = next;
357 }
358 copied.extend_from_slice(chunk);
359 }
360 OwnedBytes::new(copied)
361 }
362 #[cfg(not(target_os = "linux"))]
363 {
364 OwnedBytes::new(bytes.to_vec())
365 }
366}
367
368#[cfg(test)]
369mod tests {
370 use super::*;
371
372 #[test]
373 fn copy_pinning_preserves_unaligned_mapped_sections_tails_and_budget() {
374 let len = 5 * 128 * 1024 + 19;
375 let mut mapping = memmap2::MmapMut::map_anon(len).unwrap();
376 for (i, byte) in mapping.iter_mut().enumerate() {
377 *byte = (i % 251) as u8;
378 }
379 let source = OwnedBytes::from_mmap(Arc::new(mapping.make_read_only().unwrap()));
380 let mut section = source.slice(7..len - 5);
381 let size = section.len() as u64;
382 let mut remaining = size - 1;
383 let mut report = PinReport::default();
384 pin_section(
385 &mut section,
386 "test",
387 PinMode::Copy,
388 &mut remaining,
389 &mut report,
390 );
391 assert!(section.is_mmap());
392 assert_eq!(remaining, size - 1);
393 assert_eq!(report.skipped_budget_bytes, size);
394 remaining = size;
395 report = PinReport::default();
396 pin_section(
397 &mut section,
398 "test",
399 PinMode::Copy,
400 &mut remaining,
401 &mut report,
402 );
403 assert!(!section.is_mmap());
404 assert_eq!(section.as_slice(), &source[7..len - 5]);
405 assert_eq!(remaining, 0);
406 assert_eq!(report.pinned_bytes, size);
407 assert_eq!(report.heap_copy_bytes, size);
408 assert_eq!(report.intended_bytes, size);
409 assert_eq!(report.failed_bytes, 0);
410 }
411}