1use std::collections::BTreeMap;
2use std::path::Path;
3
4use serde::Serialize;
5use serde_json::Value;
6
7#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
13pub struct MemoryEstimate {
14 pub status: &'static str,
15 pub bytes_status: &'static str,
16 pub estimated_bytes: Option<u64>,
17 #[serde(skip_serializing_if = "Vec::is_empty")]
18 pub not_estimated: Vec<String>,
19 #[serde(flatten)]
20 pub counts: BTreeMap<String, u64>,
21}
22
23impl MemoryEstimate {
24 pub fn estimated(bytes: u64) -> Self {
25 Self {
26 status: "ready",
27 bytes_status: "estimated",
28 estimated_bytes: Some(bytes),
29 not_estimated: Vec::new(),
30 counts: BTreeMap::new(),
31 }
32 }
33
34 pub fn partial(bytes: u64) -> Self {
35 Self {
36 status: "ready",
37 bytes_status: "partial",
38 estimated_bytes: Some(bytes),
39 not_estimated: Vec::new(),
40 counts: BTreeMap::new(),
41 }
42 }
43
44 pub fn not_estimated() -> Self {
45 Self {
46 status: "ready",
47 bytes_status: "not_estimated",
48 estimated_bytes: None,
49 not_estimated: Vec::new(),
50 counts: BTreeMap::new(),
51 }
52 }
53
54 pub fn busy() -> Self {
55 Self {
56 status: "busy",
57 bytes_status: "not_estimated",
58 estimated_bytes: None,
59 not_estimated: Vec::new(),
60 counts: BTreeMap::new(),
61 }
62 }
63
64 pub fn count(mut self, name: impl Into<String>, value: usize) -> Self {
65 self.counts.insert(name.into(), usize_to_u64(value));
66 self
67 }
68
69 pub fn count_u64(mut self, name: impl Into<String>, value: u64) -> Self {
70 self.counts.insert(name.into(), value);
71 self
72 }
73
74 pub fn gap(mut self, name: impl Into<String>) -> Self {
75 self.not_estimated.push(name.into());
76 self
77 }
78}
79
80#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
81pub struct RootMemorySnapshot {
82 pub status: &'static str,
83 pub attributed_bytes: u64,
84 pub semantic: MemoryEstimate,
85 pub trigram: MemoryEstimate,
86 pub symbols: MemoryEstimate,
87 pub callgraph: MemoryEstimate,
88 pub inspect: MemoryEstimate,
89 pub bash: MemoryEstimate,
90 pub lsp: MemoryEstimate,
91 pub parser_pool: MemoryEstimate,
92}
93
94impl RootMemorySnapshot {
95 pub fn new(
96 semantic: MemoryEstimate,
97 trigram: MemoryEstimate,
98 symbols: MemoryEstimate,
99 callgraph: MemoryEstimate,
100 inspect: MemoryEstimate,
101 bash: MemoryEstimate,
102 lsp: MemoryEstimate,
103 parser_pool: MemoryEstimate,
104 ) -> Self {
105 let estimates = [
106 &semantic,
107 &trigram,
108 &symbols,
109 &callgraph,
110 &inspect,
111 &bash,
112 &lsp,
113 &parser_pool,
114 ];
115 let attributed_bytes = estimates
116 .iter()
117 .filter_map(|estimate| estimate.estimated_bytes)
118 .fold(0u64, u64::saturating_add);
119 let status = if estimates.iter().any(|estimate| estimate.status == "busy") {
120 "busy"
121 } else {
122 "ready"
123 };
124 Self {
125 status,
126 attributed_bytes,
127 semantic,
128 trigram,
129 symbols,
130 callgraph,
131 inspect,
132 bash,
133 lsp,
134 parser_pool,
135 }
136 }
137
138 pub fn busy_subsystem_count(&self) -> usize {
139 self.estimates()
140 .iter()
141 .filter(|estimate| estimate.status == "busy")
142 .count()
143 }
144
145 pub fn not_estimated_subsystem_count(&self) -> usize {
146 self.estimates()
147 .iter()
148 .filter(|estimate| estimate.estimated_bytes.is_none())
149 .count()
150 }
151
152 fn estimates(&self) -> [&MemoryEstimate; 8] {
153 [
154 &self.semantic,
155 &self.trigram,
156 &self.symbols,
157 &self.callgraph,
158 &self.inspect,
159 &self.bash,
160 &self.lsp,
161 &self.parser_pool,
162 ]
163 }
164}
165
166#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
167pub struct SqliteMemorySnapshot {
168 pub status: &'static str,
169 pub memory_used_bytes: u64,
170 pub memory_highwater_bytes: u64,
171}
172
173impl SqliteMemorySnapshot {
174 fn measure() -> Self {
175 let memory_used = unsafe { rusqlite::ffi::sqlite3_memory_used() };
178 let memory_highwater = unsafe { rusqlite::ffi::sqlite3_memory_highwater(0) };
179 Self {
180 status: "measured",
181 memory_used_bytes: nonnegative_i64_to_u64(memory_used),
182 memory_highwater_bytes: nonnegative_i64_to_u64(memory_highwater),
183 }
184 }
185}
186
187#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
188pub struct AllocatorMemorySnapshot {
189 pub status: &'static str,
190 pub bytes_in_use: Option<u64>,
191 pub size_allocated: Option<u64>,
192 pub retained_slack_bytes: Option<u64>,
193 #[serde(skip_serializing_if = "Option::is_none")]
194 pub not_estimated: Option<&'static str>,
195}
196
197impl AllocatorMemorySnapshot {
198 #[cfg(any(target_os = "macos", all(target_os = "linux", target_env = "gnu")))]
199 fn measured(bytes_in_use: u64, size_allocated: u64) -> Self {
200 Self {
201 status: "measured",
202 bytes_in_use: Some(bytes_in_use),
203 size_allocated: Some(size_allocated),
204 retained_slack_bytes: Some(size_allocated.saturating_sub(bytes_in_use)),
205 not_estimated: None,
206 }
207 }
208
209 #[cfg_attr(target_os = "macos", allow(dead_code))]
213 fn not_estimated(reason: &'static str) -> Self {
214 Self {
215 status: "not_estimated_on_this_platform",
216 bytes_in_use: None,
217 size_allocated: None,
218 retained_slack_bytes: None,
219 not_estimated: Some(reason),
220 }
221 }
222}
223
224#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
225pub struct ProcessMemorySnapshot {
226 pub rss_status: &'static str,
227 pub rss_bytes: Option<u64>,
228 #[serde(skip_serializing_if = "Option::is_none")]
237 pub phys_footprint_bytes: Option<u64>,
238 #[serde(skip_serializing_if = "Option::is_none")]
239 pub rss_not_estimated: Option<&'static str>,
240 pub sqlite: SqliteMemorySnapshot,
241 pub allocator: AllocatorMemorySnapshot,
244 pub total_attributed_bytes: u64,
245 pub unattributed_bytes: Option<i64>,
246 pub root_count: usize,
247 pub busy_subsystems: usize,
248 pub not_estimated_subsystems: usize,
249}
250
251#[derive(Debug, Clone, PartialEq, Eq)]
252pub struct AllocatorPressureRelief {
253 pub bytes_released: u64,
254 pub rss_before_bytes: Option<u64>,
255 pub rss_after_bytes: Option<u64>,
256 pub allocator_before: AllocatorMemorySnapshot,
257 pub allocator_after: AllocatorMemorySnapshot,
258}
259
260impl ProcessMemorySnapshot {
261 pub fn from_roots(
262 roots: &BTreeMap<String, RootMemorySnapshot>,
263 shared_semantic_bases: &MemoryEstimate,
264 ) -> Self {
265 let sqlite = SqliteMemorySnapshot::measure();
266 let allocator = allocator_memory_snapshot();
267 let total_attributed_bytes = roots
268 .values()
269 .map(|root| root.attributed_bytes)
270 .fold(0u64, u64::saturating_add)
271 .saturating_add(shared_semantic_bases.estimated_bytes.unwrap_or(0))
272 .saturating_add(sqlite.memory_used_bytes);
273 let busy_subsystems = roots
274 .values()
275 .map(RootMemorySnapshot::busy_subsystem_count)
276 .sum();
277 let not_estimated_subsystems = roots
278 .values()
279 .map(RootMemorySnapshot::not_estimated_subsystem_count)
280 .sum();
281 let rss_bytes = process_rss_bytes();
282 let phys_footprint_bytes = process_phys_footprint_bytes();
283 let unattributed_basis = phys_footprint_bytes.or(rss_bytes);
287 let unattributed_bytes =
288 unattributed_basis.map(|held| signed_difference(held, total_attributed_bytes));
289 Self {
290 rss_status: if rss_bytes.is_some() {
291 "estimated"
292 } else {
293 "not_estimated_on_this_platform"
294 },
295 rss_bytes,
296 phys_footprint_bytes,
297 rss_not_estimated: rss_bytes
298 .is_none()
299 .then_some("platform_process_rss_unavailable"),
300 sqlite,
301 allocator,
302 total_attributed_bytes,
303 unattributed_bytes,
304 root_count: roots.len(),
305 busy_subsystems,
306 not_estimated_subsystems,
307 }
308 }
309}
310
311pub const MEMORY_SNAPSHOT_ROOT_DETAIL_CAP: usize = 8;
318
319#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
320pub struct MemorySnapshot {
321 pub roots_status: &'static str,
322 pub roots: BTreeMap<String, RootMemorySnapshot>,
326 pub roots_total: usize,
328 pub roots_omitted: usize,
330 pub roots_omitted_bytes: u64,
333 pub shared_semantic_bases: MemoryEstimate,
335 pub process: ProcessMemorySnapshot,
336}
337
338impl MemorySnapshot {
339 pub fn new(roots_status: &'static str, roots: BTreeMap<String, RootMemorySnapshot>) -> Self {
340 let shared_semantic_bases = crate::semantic_index::shared_semantic_bases_memory();
341 let process = ProcessMemorySnapshot::from_roots(&roots, &shared_semantic_bases);
343 let roots_total = roots.len();
344 let (roots, roots_omitted, roots_omitted_bytes) =
345 cap_root_detail(roots, MEMORY_SNAPSHOT_ROOT_DETAIL_CAP);
346 Self {
347 roots_status,
348 roots,
349 roots_total,
350 roots_omitted,
351 roots_omitted_bytes,
352 shared_semantic_bases,
353 process,
354 }
355 }
356}
357
358fn cap_root_detail(
361 roots: BTreeMap<String, RootMemorySnapshot>,
362 cap: usize,
363) -> (BTreeMap<String, RootMemorySnapshot>, usize, u64) {
364 if roots.len() <= cap {
365 return (roots, 0, 0);
366 }
367 let mut entries: Vec<(String, RootMemorySnapshot)> = roots.into_iter().collect();
368 entries.sort_by(|a, b| {
370 b.1.attributed_bytes
371 .cmp(&a.1.attributed_bytes)
372 .then_with(|| a.0.cmp(&b.0))
373 });
374 let omitted: Vec<(String, RootMemorySnapshot)> = entries.split_off(cap);
375 let omitted_bytes = omitted
376 .iter()
377 .map(|(_, snapshot)| snapshot.attributed_bytes)
378 .fold(0u64, u64::saturating_add);
379 (entries.into_iter().collect(), omitted.len(), omitted_bytes)
380}
381
382#[cfg(test)]
383mod snapshot_cap_tests {
384 use super::*;
385
386 fn root_with_bytes(bytes: u64) -> RootMemorySnapshot {
387 let estimate = MemoryEstimate::estimated;
388 RootMemorySnapshot::new(
389 estimate(bytes),
390 estimate(0),
391 estimate(0),
392 estimate(0),
393 estimate(0),
394 estimate(0),
395 estimate(0),
396 estimate(0),
397 )
398 }
399
400 #[test]
401 fn detail_map_capped_but_totals_cover_all_roots() {
402 let mut roots = BTreeMap::new();
403 for i in 0..(MEMORY_SNAPSHOT_ROOT_DETAIL_CAP + 4) {
404 roots.insert(
406 format!("/root/{i:02}"),
407 root_with_bytes((i as u64 + 1) * 1000),
408 );
409 }
410 let snapshot = MemorySnapshot::new("ready", roots);
411
412 assert_eq!(snapshot.roots.len(), MEMORY_SNAPSHOT_ROOT_DETAIL_CAP);
413 assert_eq!(snapshot.roots_total, MEMORY_SNAPSHOT_ROOT_DETAIL_CAP + 4);
414 assert_eq!(snapshot.roots_omitted, 4);
415 assert_eq!(snapshot.roots_omitted_bytes, 1000 + 2000 + 3000 + 4000);
417 assert!(snapshot
419 .roots
420 .values()
421 .all(|root| root.attributed_bytes > 4000));
422 let expected_total: u64 = (1..=(MEMORY_SNAPSHOT_ROOT_DETAIL_CAP as u64 + 4))
424 .map(|i| i * 1000)
425 .sum();
426 assert!(snapshot.process.total_attributed_bytes >= expected_total);
427 }
428
429 #[test]
430 fn under_cap_keeps_everything_with_zero_omitted() {
431 let mut roots = BTreeMap::new();
432 roots.insert("/a".to_string(), root_with_bytes(10));
433 roots.insert("/b".to_string(), root_with_bytes(20));
434 let snapshot = MemorySnapshot::new("ready", roots);
435 assert_eq!(snapshot.roots.len(), 2);
436 assert_eq!(snapshot.roots_total, 2);
437 assert_eq!(snapshot.roots_omitted, 0);
438 assert_eq!(snapshot.roots_omitted_bytes, 0);
439 }
440}
441
442pub fn path_bytes(path: &Path) -> u64 {
443 #[cfg(unix)]
444 {
445 use std::os::unix::ffi::OsStrExt;
446 usize_to_u64(path.as_os_str().as_bytes().len())
447 }
448 #[cfg(windows)]
449 {
450 use std::os::windows::ffi::OsStrExt;
451 usize_to_u64(path.as_os_str().encode_wide().count())
452 .saturating_mul(std::mem::size_of::<u16>() as u64)
453 }
454 #[cfg(not(any(unix, windows)))]
455 {
456 usize_to_u64(path.to_string_lossy().len())
457 }
458}
459
460pub fn usize_to_u64(value: usize) -> u64 {
461 u64::try_from(value).unwrap_or(u64::MAX)
462}
463
464pub fn estimated_json_bytes(value: &Value) -> u64 {
465 match value {
466 Value::Null => 0,
467 Value::Bool(_) => std::mem::size_of::<bool>() as u64,
468 Value::Number(_) => std::mem::size_of::<serde_json::Number>() as u64,
469 Value::String(value) => usize_to_u64(value.len()),
470 Value::Array(values) => values
471 .iter()
472 .map(estimated_json_bytes)
473 .fold(0u64, u64::saturating_add),
474 Value::Object(values) => values.iter().fold(0u64, |bytes, (key, value)| {
475 bytes
476 .saturating_add(usize_to_u64(key.len()))
477 .saturating_add(estimated_json_bytes(value))
478 }),
479 }
480}
481
482fn signed_difference(lhs: u64, rhs: u64) -> i64 {
483 let difference = i128::from(lhs) - i128::from(rhs);
484 difference.clamp(i128::from(i64::MIN), i128::from(i64::MAX)) as i64
485}
486
487fn nonnegative_i64_to_u64(value: i64) -> u64 {
488 u64::try_from(value).unwrap_or(0)
489}
490
491#[cfg(target_os = "macos")]
492fn allocator_memory_snapshot() -> AllocatorMemorySnapshot {
493 let mut statistics = std::mem::MaybeUninit::<libc::malloc_statistics_t>::zeroed();
494 unsafe {
495 libc::malloc_zone_statistics(libc::malloc_default_zone(), statistics.as_mut_ptr());
496 }
497 let statistics = unsafe { statistics.assume_init() };
498 AllocatorMemorySnapshot::measured(
499 usize_to_u64(statistics.size_in_use),
500 usize_to_u64(statistics.size_allocated),
501 )
502}
503
504#[cfg(all(target_os = "linux", target_env = "gnu"))]
505fn allocator_memory_snapshot() -> AllocatorMemorySnapshot {
506 use std::sync::OnceLock;
512 type Mallinfo2Fn = unsafe extern "C" fn() -> libc::mallinfo2;
513 static MALLINFO2: OnceLock<Option<Mallinfo2Fn>> = OnceLock::new();
514 let resolved = MALLINFO2.get_or_init(|| {
515 let symbol = unsafe { libc::dlsym(libc::RTLD_DEFAULT, c"mallinfo2".as_ptr()) };
516 if symbol.is_null() {
517 None
518 } else {
519 Some(unsafe { std::mem::transmute::<*mut libc::c_void, Mallinfo2Fn>(symbol) })
522 }
523 });
524 let Some(mallinfo2) = resolved else {
525 return AllocatorMemorySnapshot::not_estimated("mallinfo2_requires_glibc_2_33");
526 };
527 let statistics = unsafe { mallinfo2() };
528 let mapped_bytes = statistics.hblkhd as u64;
529 let bytes_in_use = (statistics.uordblks as u64).saturating_add(mapped_bytes);
530 let size_allocated = (statistics.arena as u64).saturating_add(mapped_bytes);
531 AllocatorMemorySnapshot::measured(bytes_in_use, size_allocated)
532}
533
534#[cfg(not(any(target_os = "macos", all(target_os = "linux", target_env = "gnu"))))]
535fn allocator_memory_snapshot() -> AllocatorMemorySnapshot {
536 AllocatorMemorySnapshot::not_estimated("platform_allocator_statistics_unavailable")
537}
538
539#[cfg(target_os = "macos")]
540unsafe extern "C" {
541 fn malloc_zone_pressure_relief(zone: *mut libc::malloc_zone_t, goal: usize) -> usize;
542}
543
544pub const ALLOCATOR_SLACK_RELIEF_THRESHOLD_BYTES: u64 = 1024 * 1024 * 1024;
547
548pub const ALLOCATOR_SLACK_RELIEF_MIN_INTERVAL: std::time::Duration =
551 std::time::Duration::from_secs(300);
552
553pub fn allocator_slack_relief_due(
559 retained_slack_bytes: Option<u64>,
560 last_relief: Option<std::time::Instant>,
561 now: std::time::Instant,
562) -> bool {
563 let Some(slack) = retained_slack_bytes else {
564 return false;
565 };
566 if slack < ALLOCATOR_SLACK_RELIEF_THRESHOLD_BYTES {
567 return false;
568 }
569 match last_relief {
570 None => true,
571 Some(at) => now.duration_since(at) >= ALLOCATOR_SLACK_RELIEF_MIN_INTERVAL,
572 }
573}
574
575#[cfg(target_os = "macos")]
585pub fn spawn_allocator_slack_relief_if_due(
586 last_relief: Option<std::time::Instant>,
587 now: std::time::Instant,
588) -> bool {
589 let slack = allocator_memory_snapshot().retained_slack_bytes;
590 if !allocator_slack_relief_due(slack, last_relief, now) {
591 return false;
592 }
593 std::thread::Builder::new()
594 .name("aft-mem-relief".to_string())
595 .spawn(|| {
596 let relief = relieve_allocator_pressure();
597 log::info!(
598 "allocator slack relief: released={} rss {} -> {}",
599 relief.bytes_released,
600 relief
601 .rss_before_bytes
602 .map(|b| b.to_string())
603 .unwrap_or_else(|| "?".to_string()),
604 relief
605 .rss_after_bytes
606 .map(|b| b.to_string())
607 .unwrap_or_else(|| "?".to_string()),
608 );
609 })
610 .is_ok()
611}
612
613#[cfg(target_os = "macos")]
616pub fn relieve_allocator_pressure() -> AllocatorPressureRelief {
617 let rss_before_bytes = process_rss_bytes();
618 let allocator_before = allocator_memory_snapshot();
619 let bytes_released = unsafe { malloc_zone_pressure_relief(std::ptr::null_mut(), 0) };
620 let allocator_after = allocator_memory_snapshot();
621 let rss_after_bytes = process_rss_bytes();
622 AllocatorPressureRelief {
623 bytes_released: usize_to_u64(bytes_released),
624 rss_before_bytes,
625 rss_after_bytes,
626 allocator_before,
627 allocator_after,
628 }
629}
630
631#[cfg(target_os = "macos")]
632fn process_rss_bytes() -> Option<u64> {
633 let mut info = std::mem::MaybeUninit::<libc::proc_taskinfo>::zeroed();
634 let size = std::mem::size_of::<libc::proc_taskinfo>();
635 let written = unsafe {
636 libc::proc_pidinfo(
637 libc::getpid(),
638 libc::PROC_PIDTASKINFO,
639 0,
640 info.as_mut_ptr().cast(),
641 i32::try_from(size).ok()?,
642 )
643 };
644 if written != i32::try_from(size).ok()? {
645 return None;
646 }
647 Some(unsafe { info.assume_init() }.pti_resident_size)
648}
649
650#[cfg(target_os = "macos")]
653fn process_phys_footprint_bytes() -> Option<u64> {
654 let mut usage = std::mem::MaybeUninit::<libc::rusage_info_v4>::zeroed();
655 let rc = unsafe {
656 libc::proc_pid_rusage(
657 libc::getpid(),
658 libc::RUSAGE_INFO_V4,
659 usage.as_mut_ptr().cast(),
660 )
661 };
662 if rc != 0 {
663 return None;
664 }
665 Some(unsafe { usage.assume_init() }.ri_phys_footprint)
666}
667
668#[cfg(not(target_os = "macos"))]
669fn process_phys_footprint_bytes() -> Option<u64> {
670 None
671}
672
673#[cfg(target_os = "linux")]
674fn process_rss_bytes() -> Option<u64> {
675 let statm = std::fs::read_to_string("/proc/self/statm").ok()?;
676 let resident_pages = statm.split_whitespace().nth(1)?.parse::<u64>().ok()?;
677 let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
678 if page_size <= 0 {
679 return None;
680 }
681 resident_pages.checked_mul(page_size as u64)
682}
683
684#[cfg(not(any(target_os = "macos", target_os = "linux")))]
685fn process_rss_bytes() -> Option<u64> {
686 None
687}
688
689#[cfg(test)]
690mod tests {
691 use super::*;
692
693 #[test]
694 fn process_snapshot_preserves_negative_residuals() {
695 assert_eq!(signed_difference(5, 8), -3);
696 }
697
698 #[test]
699 fn slack_relief_fires_on_large_slack_and_respects_spacing() {
700 use std::time::{Duration, Instant};
701 let now = Instant::now();
702 let big = Some(ALLOCATOR_SLACK_RELIEF_THRESHOLD_BYTES);
703 assert!(!allocator_slack_relief_due(None, None, now));
705 assert!(!allocator_slack_relief_due(
707 Some(ALLOCATOR_SLACK_RELIEF_THRESHOLD_BYTES - 1),
708 None,
709 now
710 ));
711 assert!(allocator_slack_relief_due(big, None, now));
713 let recent = now - Duration::from_secs(10);
715 assert!(!allocator_slack_relief_due(big, Some(recent), now));
716 let stale = now - ALLOCATOR_SLACK_RELIEF_MIN_INTERVAL;
718 assert!(allocator_slack_relief_due(big, Some(stale), now));
719 }
720
721 #[test]
722 fn json_estimator_scales_with_payload_content() {
723 let empty = estimated_json_bytes(&serde_json::json!({}));
724 let populated = estimated_json_bytes(&serde_json::json!({"message": "hello"}));
725 assert_eq!(empty, 0);
726 assert!(populated >= 12);
727 }
728
729 #[test]
730 fn process_snapshot_exposes_sqlite_and_allocator_sections() {
731 let shared = MemoryEstimate::estimated(7);
732 let snapshot = ProcessMemorySnapshot::from_roots(&BTreeMap::new(), &shared);
733 assert_eq!(snapshot.sqlite.status, "measured");
734 assert!(snapshot.sqlite.memory_highwater_bytes >= snapshot.sqlite.memory_used_bytes);
735 assert_eq!(
736 snapshot.total_attributed_bytes,
737 snapshot.sqlite.memory_used_bytes.saturating_add(7)
738 );
739
740 let serialized = serde_json::to_value(&snapshot).expect("serialize process memory");
741 assert!(serialized["sqlite"]["memory_used_bytes"].is_u64());
742 assert!(serialized["allocator"].get("bytes_in_use").is_some());
743 assert!(serialized["allocator"].get("size_allocated").is_some());
744 assert!(serialized["allocator"]
745 .get("retained_slack_bytes")
746 .is_some());
747 }
748
749 #[cfg(any(target_os = "macos", all(target_os = "linux", target_env = "gnu")))]
750 #[test]
751 fn allocator_snapshot_reports_measured_slack() {
752 let allocator = allocator_memory_snapshot();
753 assert_eq!(allocator.status, "measured");
754 let in_use = allocator.bytes_in_use.expect("allocator bytes in use");
755 let allocated = allocator.size_allocated.expect("allocator size allocated");
756 assert_eq!(
757 allocator.retained_slack_bytes,
758 Some(allocated.saturating_sub(in_use))
759 );
760 }
761
762 #[cfg(not(any(target_os = "macos", all(target_os = "linux", target_env = "gnu"))))]
763 #[test]
764 fn allocator_snapshot_is_honest_when_platform_counters_are_unavailable() {
765 let allocator = allocator_memory_snapshot();
766 assert_eq!(allocator.status, "not_estimated_on_this_platform");
767 assert_eq!(allocator.bytes_in_use, None);
768 assert_eq!(allocator.size_allocated, None);
769 assert_eq!(allocator.retained_slack_bytes, None);
770 assert_eq!(
771 allocator.not_estimated,
772 Some("platform_allocator_statistics_unavailable")
773 );
774 }
775
776 #[cfg(target_os = "macos")]
777 #[test]
778 #[ignore = "bounded live RSS experiment; run explicitly after allocator changes"]
779 fn allocator_pressure_relief_warm_then_idle_measurement() {
780 let warm_pages = (0..16 * 1024)
781 .map(|seed| {
782 let mut page = Box::new([0u8; 4096]);
783 page[0] = seed as u8;
784 page
785 })
786 .collect::<Vec<_>>();
787 std::hint::black_box(&warm_pages);
788 drop(warm_pages);
789
790 let relief = relieve_allocator_pressure();
791 let sqlite = SqliteMemorySnapshot::measure();
792 eprintln!(
793 "warm-then-idle pressure relief: rss_before={:?} rss_after={:?} allocator_in_use_before={:?} allocator_in_use_after={:?} allocator_allocated_before={:?} allocator_allocated_after={:?} allocator_slack_before={:?} allocator_slack_after={:?} allocator_reported_released={} sqlite_used={} sqlite_highwater={}",
794 relief.rss_before_bytes,
795 relief.rss_after_bytes,
796 relief.allocator_before.bytes_in_use,
797 relief.allocator_after.bytes_in_use,
798 relief.allocator_before.size_allocated,
799 relief.allocator_after.size_allocated,
800 relief.allocator_before.retained_slack_bytes,
801 relief.allocator_after.retained_slack_bytes,
802 relief.bytes_released,
803 sqlite.memory_used_bytes,
804 sqlite.memory_highwater_bytes,
805 );
806 assert_eq!(relief.allocator_before.status, "measured");
807 assert_eq!(relief.allocator_after.status, "measured");
808 }
809}