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 callgraph_projection: MemoryEstimate,
89 pub inspect: MemoryEstimate,
90 pub bash: MemoryEstimate,
91 pub lsp: MemoryEstimate,
92 pub parser_pool: MemoryEstimate,
93}
94
95#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
96pub(crate) struct RootMemoryRollup {
97 pub(crate) status: &'static str,
98 pub(crate) attributed_bytes: u64,
99 pub(crate) busy_subsystems: usize,
100 pub(crate) not_estimated_subsystems: usize,
101 #[serde(skip_serializing_if = "Option::is_none")]
105 pub(crate) standing: Option<bool>,
106}
107
108impl RootMemoryRollup {
109 pub(crate) fn from_estimates(estimates: &[&MemoryEstimate]) -> Self {
110 let attributed_bytes = estimates
111 .iter()
112 .filter_map(|estimate| estimate.estimated_bytes)
113 .fold(0u64, u64::saturating_add);
114 let busy_subsystems = estimates
115 .iter()
116 .filter(|estimate| estimate.status == "busy")
117 .count();
118 let not_estimated_subsystems = estimates
119 .iter()
120 .filter(|estimate| estimate.estimated_bytes.is_none())
121 .count();
122 Self {
123 status: if busy_subsystems > 0 { "busy" } else { "ready" },
124 attributed_bytes,
125 busy_subsystems,
126 not_estimated_subsystems,
127 standing: None,
128 }
129 }
130
131 pub(crate) fn with_standing(mut self) -> Self {
133 self.standing = Some(true);
134 self
135 }
136}
137
138impl RootMemorySnapshot {
139 pub fn new(
140 semantic: MemoryEstimate,
141 trigram: MemoryEstimate,
142 symbols: MemoryEstimate,
143 callgraph: MemoryEstimate,
144 callgraph_projection: MemoryEstimate,
145 inspect: MemoryEstimate,
146 bash: MemoryEstimate,
147 lsp: MemoryEstimate,
148 parser_pool: MemoryEstimate,
149 ) -> Self {
150 let rollup = RootMemoryRollup::from_estimates(&[
151 &semantic,
152 &trigram,
153 &symbols,
154 &callgraph,
155 &callgraph_projection,
156 &inspect,
157 &bash,
158 &lsp,
159 &parser_pool,
160 ]);
161 Self {
162 status: rollup.status,
163 attributed_bytes: rollup.attributed_bytes,
164 semantic,
165 trigram,
166 symbols,
167 callgraph,
168 callgraph_projection,
169 inspect,
170 bash,
171 lsp,
172 parser_pool,
173 }
174 }
175
176 fn rollup(&self) -> RootMemoryRollup {
177 RootMemoryRollup::from_estimates(&[
178 &self.semantic,
179 &self.trigram,
180 &self.symbols,
181 &self.callgraph,
182 &self.callgraph_projection,
183 &self.inspect,
184 &self.bash,
185 &self.lsp,
186 &self.parser_pool,
187 ])
188 }
189
190 pub fn busy_subsystem_count(&self) -> usize {
191 self.rollup().busy_subsystems
192 }
193
194 pub fn not_estimated_subsystem_count(&self) -> usize {
195 self.rollup().not_estimated_subsystems
196 }
197
198 pub fn evictable_bytes(&self) -> u64 {
202 [
203 &self.trigram,
204 &self.semantic,
205 &self.symbols,
206 &self.callgraph,
207 &self.inspect,
208 ]
209 .into_iter()
210 .filter_map(|estimate| estimate.estimated_bytes)
211 .fold(0, u64::saturating_add)
212 }
213}
214
215#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
216pub struct SqliteMemorySnapshot {
217 pub status: &'static str,
218 pub memory_used_bytes: u64,
219 pub memory_highwater_bytes: u64,
220}
221
222impl SqliteMemorySnapshot {
223 fn measure() -> Self {
224 let memory_used = unsafe { rusqlite::ffi::sqlite3_memory_used() };
227 let memory_highwater = unsafe { rusqlite::ffi::sqlite3_memory_highwater(0) };
228 Self {
229 status: "measured",
230 memory_used_bytes: nonnegative_i64_to_u64(memory_used),
231 memory_highwater_bytes: nonnegative_i64_to_u64(memory_highwater),
232 }
233 }
234}
235
236#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
237pub struct AllocatorMemorySnapshot {
238 pub status: &'static str,
239 pub bytes_in_use: Option<u64>,
240 pub size_allocated: Option<u64>,
241 pub retained_slack_bytes: Option<u64>,
242 #[serde(skip_serializing_if = "Option::is_none")]
243 pub not_estimated: Option<&'static str>,
244}
245
246impl AllocatorMemorySnapshot {
247 #[cfg(any(target_os = "macos", all(target_os = "linux", target_env = "gnu")))]
248 fn measured(bytes_in_use: u64, size_allocated: u64) -> Self {
249 Self {
250 status: "measured",
251 bytes_in_use: Some(bytes_in_use),
252 size_allocated: Some(size_allocated),
253 retained_slack_bytes: Some(size_allocated.saturating_sub(bytes_in_use)),
254 not_estimated: None,
255 }
256 }
257
258 #[cfg_attr(target_os = "macos", allow(dead_code))]
262 fn not_estimated(reason: &'static str) -> Self {
263 Self {
264 status: "not_estimated_on_this_platform",
265 bytes_in_use: None,
266 size_allocated: None,
267 retained_slack_bytes: None,
268 not_estimated: Some(reason),
269 }
270 }
271}
272
273#[derive(Debug, Clone, Copy, PartialEq, Eq)]
274pub enum AllocatorSource {
275 Cached,
277 Measure,
279}
280
281#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
282pub struct ProcessMemorySnapshot {
283 pub rss_status: &'static str,
284 pub rss_bytes: Option<u64>,
285 #[serde(skip_serializing_if = "Option::is_none")]
294 pub phys_footprint_bytes: Option<u64>,
295 #[serde(skip_serializing_if = "Option::is_none")]
296 pub rss_not_estimated: Option<&'static str>,
297 pub sqlite: SqliteMemorySnapshot,
298 pub allocator: AllocatorMemorySnapshot,
301 pub allocator_slack_measured: bool,
303 pub allocator_observation_age_ms: Option<u64>,
306 pub total_attributed_bytes: u64,
307 pub unattributed_bytes: Option<i64>,
308 pub root_count: usize,
309 pub busy_subsystems: usize,
310 pub not_estimated_subsystems: usize,
311}
312
313#[derive(Debug, Clone, PartialEq, Eq)]
314pub struct AllocatorPressureRelief {
315 pub bytes_released: u64,
316 pub rss_before_bytes: Option<u64>,
317 pub rss_after_bytes: Option<u64>,
318 pub allocator_before: AllocatorMemorySnapshot,
319 pub allocator_after: AllocatorMemorySnapshot,
320}
321
322impl ProcessMemorySnapshot {
323 pub fn from_roots(
324 roots: &BTreeMap<String, RootMemorySnapshot>,
325 shared_semantic_bases: &MemoryEstimate,
326 ) -> Self {
327 let rollups: Vec<_> = roots.values().map(RootMemorySnapshot::rollup).collect();
328 Self::from_root_rollups(
329 rollups.iter(),
330 roots.len(),
331 shared_semantic_bases,
332 AllocatorSource::Measure,
333 )
334 }
335
336 fn from_root_rollups<'a>(
337 roots: impl Iterator<Item = &'a RootMemoryRollup>,
338 root_count: usize,
339 shared_semantic_bases: &MemoryEstimate,
340 allocator_source: AllocatorSource,
341 ) -> Self {
342 let mut root_attributed_bytes = 0u64;
343 let mut busy_subsystems = 0usize;
344 let mut not_estimated_subsystems = 0usize;
345 for root in roots {
346 root_attributed_bytes = root_attributed_bytes.saturating_add(root.attributed_bytes);
347 busy_subsystems = busy_subsystems.saturating_add(root.busy_subsystems);
348 not_estimated_subsystems =
349 not_estimated_subsystems.saturating_add(root.not_estimated_subsystems);
350 }
351 let sqlite = SqliteMemorySnapshot::measure();
352 let (allocator, allocator_observation_age_ms) = allocator_observation(allocator_source);
353 let total_attributed_bytes = root_attributed_bytes
354 .saturating_add(shared_semantic_bases.estimated_bytes.unwrap_or(0))
355 .saturating_add(sqlite.memory_used_bytes);
356 let rss_bytes = process_rss_bytes();
357 let phys_footprint_bytes = process_phys_footprint_bytes();
358 let unattributed_basis = phys_footprint_bytes.or(rss_bytes);
362 let unattributed_bytes =
363 unattributed_basis.map(|held| signed_difference(held, total_attributed_bytes));
364 Self {
365 rss_status: if rss_bytes.is_some() {
366 "estimated"
367 } else {
368 "not_estimated_on_this_platform"
369 },
370 rss_bytes,
371 phys_footprint_bytes,
372 rss_not_estimated: rss_bytes
373 .is_none()
374 .then_some("platform_process_rss_unavailable"),
375 sqlite,
376 allocator_slack_measured: allocator.retained_slack_bytes.is_some(),
377 allocator,
378 allocator_observation_age_ms,
379 total_attributed_bytes,
380 unattributed_bytes,
381 root_count,
382 busy_subsystems,
383 not_estimated_subsystems,
384 }
385 }
386}
387
388pub const MEMORY_SNAPSHOT_ROOT_DETAIL_CAP: usize = 8;
395
396#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
397pub struct MemorySnapshot {
398 pub roots_status: &'static str,
399 pub roots: BTreeMap<String, RootMemorySnapshot>,
403 pub roots_total: usize,
405 pub roots_omitted: usize,
407 pub roots_omitted_bytes: u64,
410 pub shared_semantic_bases: MemoryEstimate,
412 pub process: ProcessMemorySnapshot,
413}
414
415#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
416pub(crate) struct MemoryRollupSnapshot {
417 pub(crate) roots_status: &'static str,
418 pub(crate) roots: BTreeMap<String, RootMemoryRollup>,
419 pub(crate) roots_total: usize,
420 pub(crate) roots_omitted: usize,
421 pub(crate) roots_omitted_bytes: u64,
422 pub(crate) process: ProcessMemorySnapshot,
423}
424
425impl MemoryRollupSnapshot {
426 pub(crate) fn new(
427 roots_status: &'static str,
428 roots: BTreeMap<String, RootMemoryRollup>,
429 ) -> Self {
430 let shared_semantic_bases = crate::semantic_index::shared_semantic_bases_memory();
431 let process = ProcessMemorySnapshot::from_root_rollups(
432 roots.values(),
433 roots.len(),
434 &shared_semantic_bases,
435 AllocatorSource::Measure,
436 );
437 let roots_total = roots.len();
438 let (roots, roots_omitted, roots_omitted_bytes) =
439 cap_root_rollups(roots, MEMORY_SNAPSHOT_ROOT_DETAIL_CAP);
440 Self {
441 roots_status,
442 roots,
443 roots_total,
444 roots_omitted,
445 roots_omitted_bytes,
446 process,
447 }
448 }
449}
450
451impl MemorySnapshot {
452 pub fn new(roots_status: &'static str, roots: BTreeMap<String, RootMemorySnapshot>) -> Self {
453 Self::with_cap(roots_status, roots, true, AllocatorSource::Cached)
454 }
455
456 pub fn new_uncapped(
458 roots_status: &'static str,
459 roots: BTreeMap<String, RootMemorySnapshot>,
460 ) -> Self {
461 Self::with_cap(roots_status, roots, false, AllocatorSource::Measure)
462 }
463
464 fn with_cap(
465 roots_status: &'static str,
466 roots: BTreeMap<String, RootMemorySnapshot>,
467 cap_detail: bool,
468 allocator_source: AllocatorSource,
469 ) -> Self {
470 let shared_semantic_bases = crate::semantic_index::shared_semantic_bases_memory();
471 let rollups: Vec<_> = roots.values().map(RootMemorySnapshot::rollup).collect();
473 let process = ProcessMemorySnapshot::from_root_rollups(
474 rollups.iter(),
475 roots.len(),
476 &shared_semantic_bases,
477 allocator_source,
478 );
479 let roots_total = roots.len();
480 let (roots, roots_omitted, roots_omitted_bytes) = if cap_detail {
481 cap_root_detail(roots, MEMORY_SNAPSHOT_ROOT_DETAIL_CAP)
482 } else {
483 (roots, 0, 0)
484 };
485 Self {
486 roots_status,
487 roots,
488 roots_total,
489 roots_omitted,
490 roots_omitted_bytes,
491 shared_semantic_bases,
492 process,
493 }
494 }
495}
496
497fn cap_roots<T>(
498 roots: BTreeMap<String, T>,
499 cap: usize,
500 attributed_bytes: impl Fn(&T) -> u64,
501) -> (BTreeMap<String, T>, usize, u64) {
502 if roots.len() <= cap {
503 return (roots, 0, 0);
504 }
505 let mut entries: Vec<_> = roots.into_iter().collect();
506 entries.sort_by(|a, b| {
507 attributed_bytes(&b.1)
508 .cmp(&attributed_bytes(&a.1))
509 .then_with(|| a.0.cmp(&b.0))
510 });
511 let omitted = entries.split_off(cap);
512 let omitted_bytes = omitted
513 .iter()
514 .map(|(_, snapshot)| attributed_bytes(snapshot))
515 .fold(0u64, u64::saturating_add);
516 (entries.into_iter().collect(), omitted.len(), omitted_bytes)
517}
518
519fn cap_root_rollups(
520 roots: BTreeMap<String, RootMemoryRollup>,
521 cap: usize,
522) -> (BTreeMap<String, RootMemoryRollup>, usize, u64) {
523 cap_roots(roots, cap, |snapshot| snapshot.attributed_bytes)
524}
525
526fn cap_root_detail(
529 roots: BTreeMap<String, RootMemorySnapshot>,
530 cap: usize,
531) -> (BTreeMap<String, RootMemorySnapshot>, usize, u64) {
532 cap_roots(roots, cap, |snapshot| snapshot.attributed_bytes)
533}
534
535#[cfg(test)]
536mod snapshot_cap_tests {
537 use super::*;
538
539 fn root_with_bytes(bytes: u64) -> RootMemorySnapshot {
540 let estimate = MemoryEstimate::estimated;
541 RootMemorySnapshot::new(
542 estimate(bytes),
543 estimate(0),
544 estimate(0),
545 estimate(0),
546 estimate(0),
547 estimate(0),
548 estimate(0),
549 estimate(0),
550 estimate(0),
551 )
552 }
553
554 #[test]
555 fn detail_map_capped_but_totals_cover_all_roots() {
556 let mut roots = BTreeMap::new();
557 for i in 0..(MEMORY_SNAPSHOT_ROOT_DETAIL_CAP + 4) {
558 roots.insert(
560 format!("/root/{i:02}"),
561 root_with_bytes((i as u64 + 1) * 1000),
562 );
563 }
564 let snapshot = MemorySnapshot::new("ready", roots);
565
566 assert_eq!(snapshot.roots.len(), MEMORY_SNAPSHOT_ROOT_DETAIL_CAP);
567 assert_eq!(snapshot.roots_total, MEMORY_SNAPSHOT_ROOT_DETAIL_CAP + 4);
568 assert_eq!(snapshot.roots_omitted, 4);
569 assert_eq!(snapshot.roots_omitted_bytes, 1000 + 2000 + 3000 + 4000);
571 assert!(snapshot
573 .roots
574 .values()
575 .all(|root| root.attributed_bytes > 4000));
576 let expected_total: u64 = (1..=(MEMORY_SNAPSHOT_ROOT_DETAIL_CAP as u64 + 4))
578 .map(|i| i * 1000)
579 .sum();
580 assert!(snapshot.process.total_attributed_bytes >= expected_total);
581 }
582
583 #[test]
584 fn rollup_cap_keeps_only_top_roots_while_totals_cover_all() {
585 let mut roots = BTreeMap::new();
586 for i in 0..(MEMORY_SNAPSHOT_ROOT_DETAIL_CAP + 4) {
587 roots.insert(
588 format!("/rollup/{i:02}"),
589 root_with_bytes((i as u64 + 1) * 1000).rollup(),
590 );
591 }
592 let snapshot = MemoryRollupSnapshot::new("ready", roots);
593
594 assert_eq!(snapshot.roots.len(), MEMORY_SNAPSHOT_ROOT_DETAIL_CAP);
595 assert_eq!(snapshot.roots_total, MEMORY_SNAPSHOT_ROOT_DETAIL_CAP + 4);
596 assert_eq!(snapshot.roots_omitted, 4);
597 assert_eq!(snapshot.roots_omitted_bytes, 1000 + 2000 + 3000 + 4000);
598 let expected_total: u64 = (1..=(MEMORY_SNAPSHOT_ROOT_DETAIL_CAP as u64 + 4))
599 .map(|i| i * 1000)
600 .sum();
601 assert!(snapshot.process.total_attributed_bytes >= expected_total);
602 }
603
604 #[test]
605 fn standing_rollup_uses_the_shared_top_eight_cap() {
606 let mut roots = BTreeMap::new();
607 for i in 0..=MEMORY_SNAPSHOT_ROOT_DETAIL_CAP {
608 let rollup = root_with_bytes((i as u64 + 1) * 1000).rollup();
609 roots.insert(format!("/root/{i:02}"), rollup);
610 }
611 roots.insert(
612 "standing-artifact-key".to_string(),
613 root_with_bytes(20_000).rollup().with_standing(),
614 );
615
616 let snapshot = MemoryRollupSnapshot::new("ready", roots);
617 assert_eq!(snapshot.roots.len(), MEMORY_SNAPSHOT_ROOT_DETAIL_CAP);
618 assert_eq!(snapshot.roots_total, MEMORY_SNAPSHOT_ROOT_DETAIL_CAP + 2);
619 assert_eq!(snapshot.roots_omitted, 2);
620 assert_eq!(
621 snapshot.roots["standing-artifact-key"].standing,
622 Some(true),
623 "standing memory stays in the ordinary per-root table"
624 );
625 }
626
627 #[test]
628 fn under_cap_keeps_everything_with_zero_omitted() {
629 let mut roots = BTreeMap::new();
630 roots.insert("/a".to_string(), root_with_bytes(10));
631 roots.insert("/b".to_string(), root_with_bytes(20));
632 let snapshot = MemorySnapshot::new("ready", roots);
633 assert_eq!(snapshot.roots.len(), 2);
634 assert_eq!(snapshot.roots_total, 2);
635 assert_eq!(snapshot.roots_omitted, 0);
636 assert_eq!(snapshot.roots_omitted_bytes, 0);
637 }
638}
639
640pub fn path_bytes(path: &Path) -> u64 {
641 #[cfg(unix)]
642 {
643 use std::os::unix::ffi::OsStrExt;
644 usize_to_u64(path.as_os_str().as_bytes().len())
645 }
646 #[cfg(windows)]
647 {
648 use std::os::windows::ffi::OsStrExt;
649 usize_to_u64(path.as_os_str().encode_wide().count())
650 .saturating_mul(std::mem::size_of::<u16>() as u64)
651 }
652 #[cfg(not(any(unix, windows)))]
653 {
654 usize_to_u64(path.to_string_lossy().len())
655 }
656}
657
658pub fn usize_to_u64(value: usize) -> u64 {
659 u64::try_from(value).unwrap_or(u64::MAX)
660}
661
662pub fn estimated_json_bytes(value: &Value) -> u64 {
663 match value {
664 Value::Null => 0,
665 Value::Bool(_) => std::mem::size_of::<bool>() as u64,
666 Value::Number(_) => std::mem::size_of::<serde_json::Number>() as u64,
667 Value::String(value) => usize_to_u64(value.len()),
668 Value::Array(values) => values
669 .iter()
670 .map(estimated_json_bytes)
671 .fold(0u64, u64::saturating_add),
672 Value::Object(values) => values.iter().fold(0u64, |bytes, (key, value)| {
673 bytes
674 .saturating_add(usize_to_u64(key.len()))
675 .saturating_add(estimated_json_bytes(value))
676 }),
677 }
678}
679
680fn signed_difference(lhs: u64, rhs: u64) -> i64 {
681 let difference = i128::from(lhs) - i128::from(rhs);
682 difference.clamp(i128::from(i64::MIN), i128::from(i64::MAX)) as i64
683}
684
685fn nonnegative_i64_to_u64(value: i64) -> u64 {
686 u64::try_from(value).unwrap_or(0)
687}
688
689#[cfg(target_os = "macos")]
690fn allocator_memory_snapshot_impl() -> AllocatorMemorySnapshot {
691 let mut statistics = std::mem::MaybeUninit::<libc::malloc_statistics_t>::zeroed();
692 unsafe {
693 libc::malloc_zone_statistics(libc::malloc_default_zone(), statistics.as_mut_ptr());
694 }
695 let statistics = unsafe { statistics.assume_init() };
696 AllocatorMemorySnapshot::measured(
697 usize_to_u64(statistics.size_in_use),
698 usize_to_u64(statistics.size_allocated),
699 )
700}
701
702#[cfg(all(target_os = "linux", target_env = "gnu"))]
703fn allocator_memory_snapshot_impl() -> AllocatorMemorySnapshot {
704 use std::sync::OnceLock;
710 type Mallinfo2Fn = unsafe extern "C" fn() -> libc::mallinfo2;
711 static MALLINFO2: OnceLock<Option<Mallinfo2Fn>> = OnceLock::new();
712 let resolved = MALLINFO2.get_or_init(|| {
713 let symbol = unsafe { libc::dlsym(libc::RTLD_DEFAULT, c"mallinfo2".as_ptr()) };
714 if symbol.is_null() {
715 None
716 } else {
717 Some(unsafe { std::mem::transmute::<*mut libc::c_void, Mallinfo2Fn>(symbol) })
720 }
721 });
722 let Some(mallinfo2) = resolved else {
723 return AllocatorMemorySnapshot::not_estimated("mallinfo2_requires_glibc_2_33");
724 };
725 let statistics = unsafe { mallinfo2() };
726 let mapped_bytes = statistics.hblkhd as u64;
727 let bytes_in_use = (statistics.uordblks as u64).saturating_add(mapped_bytes);
728 let size_allocated = (statistics.arena as u64).saturating_add(mapped_bytes);
729 AllocatorMemorySnapshot::measured(bytes_in_use, size_allocated)
730}
731
732#[cfg(all(target_os = "linux", target_env = "gnu"))]
733type MallocTrimFn = unsafe extern "C" fn(libc::size_t) -> libc::c_int;
734
735#[cfg(all(target_os = "linux", target_env = "gnu"))]
738fn resolved_malloc_trim() -> Option<MallocTrimFn> {
739 use std::sync::OnceLock;
740 static MALLOC_TRIM: OnceLock<Option<MallocTrimFn>> = OnceLock::new();
741 MALLOC_TRIM
742 .get_or_init(|| {
743 let symbol = unsafe { libc::dlsym(libc::RTLD_DEFAULT, c"malloc_trim".as_ptr()) };
744 if symbol.is_null() {
745 None
746 } else {
747 Some(unsafe { std::mem::transmute::<*mut libc::c_void, MallocTrimFn>(symbol) })
750 }
751 })
752 .as_ref()
753 .copied()
754}
755
756#[cfg(not(any(target_os = "macos", all(target_os = "linux", target_env = "gnu"))))]
757fn allocator_memory_snapshot_impl() -> AllocatorMemorySnapshot {
758 AllocatorMemorySnapshot::not_estimated("platform_allocator_statistics_unavailable")
759}
760
761fn allocator_memory_snapshot() -> AllocatorMemorySnapshot {
765 #[cfg(test)]
766 {
767 THREAD_ALLOCATOR_SNAPSHOT_CALLS.with(|calls| calls.set(calls.get() + 1));
768 let name = std::thread::current()
769 .name()
770 .unwrap_or("unnamed")
771 .to_string();
772 ALLOCATOR_SNAPSHOT_THREADS
773 .lock()
774 .expect("allocator snapshot test thread log")
775 .push(name);
776 }
777 allocator_memory_snapshot_impl()
778}
779
780fn allocator_observation(source: AllocatorSource) -> (AllocatorMemorySnapshot, Option<u64>) {
781 match source {
782 AllocatorSource::Cached => cached_allocator_observation()
783 .map(|(snapshot, age_ms)| (snapshot, Some(age_ms)))
784 .unwrap_or_else(|| {
785 (
786 AllocatorMemorySnapshot::not_estimated("allocator_observation_unavailable"),
787 None,
788 )
789 }),
790 AllocatorSource::Measure => measure_allocator_observation(),
791 }
792}
793
794fn measure_allocator_observation() -> (AllocatorMemorySnapshot, Option<u64>) {
795 #[cfg(any(target_os = "macos", target_os = "linux"))]
798 let sampled_at = std::time::Instant::now();
799 let snapshot = allocator_memory_snapshot();
800 #[cfg(any(target_os = "macos", target_os = "linux"))]
801 {
802 let sampled_at_ms = allocator_slack_elapsed_ms(sampled_at);
803 publish_allocator_slack(&snapshot, sampled_at_ms);
804 let age_ms =
805 allocator_slack_elapsed_ms(std::time::Instant::now()).saturating_sub(sampled_at_ms);
806 (snapshot, Some(age_ms))
807 }
808 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
809 {
810 (snapshot, None)
811 }
812}
813
814#[cfg(target_os = "macos")]
815unsafe extern "C" {
816 fn malloc_zone_pressure_relief(zone: *mut libc::malloc_zone_t, goal: usize) -> usize;
817}
818
819pub const ALLOCATOR_SLACK_RELIEF_THRESHOLD_BYTES: u64 = 1024 * 1024 * 1024;
822
823pub const ALLOCATOR_SLACK_RELIEF_MIN_INTERVAL: std::time::Duration =
826 std::time::Duration::from_secs(300);
827
828#[cfg(any(target_os = "macos", target_os = "linux"))]
829const ALLOCATOR_SLACK_SAMPLE_INTERVAL: std::time::Duration = std::time::Duration::from_secs(60);
834#[cfg(any(target_os = "macos", target_os = "linux"))]
835const UNKNOWN_ALLOCATOR_SLACK: u64 = u64::MAX;
836#[cfg(any(target_os = "macos", target_os = "linux"))]
837const UNSAMPLED_AT_MS: u64 = u64::MAX;
838#[cfg(any(target_os = "macos", target_os = "linux"))]
839const ALLOCATOR_OBSERVATION_UNSAMPLED: u8 = 0;
840#[cfg(any(target_os = "macos", target_os = "linux"))]
841const ALLOCATOR_OBSERVATION_MEASURED: u8 = 1;
842#[cfg(any(target_os = "macos", target_os = "linux"))]
843const ALLOCATOR_OBSERVATION_NOT_ESTIMATED: u8 = 2;
844#[cfg(any(target_os = "macos", target_os = "linux"))]
845static ALLOCATOR_SLACK_CACHE_ORIGIN: std::sync::OnceLock<std::time::Instant> =
846 std::sync::OnceLock::new();
847#[cfg(any(target_os = "macos", target_os = "linux"))]
848static LAST_OBSERVED_ALLOCATOR_SLACK_BYTES: std::sync::atomic::AtomicU64 =
849 std::sync::atomic::AtomicU64::new(UNKNOWN_ALLOCATOR_SLACK);
850#[cfg(any(target_os = "macos", target_os = "linux"))]
851static LAST_OBSERVED_ALLOCATOR_BYTES_IN_USE: std::sync::atomic::AtomicU64 =
852 std::sync::atomic::AtomicU64::new(UNKNOWN_ALLOCATOR_SLACK);
853#[cfg(any(target_os = "macos", target_os = "linux"))]
854static LAST_OBSERVED_ALLOCATOR_SIZE_ALLOCATED: std::sync::atomic::AtomicU64 =
855 std::sync::atomic::AtomicU64::new(UNKNOWN_ALLOCATOR_SLACK);
856#[cfg(any(target_os = "macos", target_os = "linux"))]
857static LAST_ALLOCATOR_OBSERVATION_STATUS: std::sync::atomic::AtomicU8 =
858 std::sync::atomic::AtomicU8::new(ALLOCATOR_OBSERVATION_UNSAMPLED);
859#[cfg(any(target_os = "macos", target_os = "linux"))]
860static LAST_ALLOCATOR_OBSERVATION_AT_MS: std::sync::atomic::AtomicU64 =
861 std::sync::atomic::AtomicU64::new(UNSAMPLED_AT_MS);
862#[cfg(any(target_os = "macos", target_os = "linux"))]
863static ALLOCATOR_OBSERVATION_SEQUENCE: std::sync::atomic::AtomicU64 =
864 std::sync::atomic::AtomicU64::new(0);
865#[cfg(any(target_os = "macos", target_os = "linux"))]
866static ALLOCATOR_OBSERVATION_PUBLISH_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
867#[cfg(any(target_os = "macos", target_os = "linux"))]
868static LAST_ALLOCATOR_SLACK_SAMPLE_AT_MS: std::sync::atomic::AtomicU64 =
869 std::sync::atomic::AtomicU64::new(UNSAMPLED_AT_MS);
870#[cfg(any(target_os = "macos", target_os = "linux"))]
871static LAST_ALLOCATOR_SLACK_RELIEF_AT_MS: std::sync::atomic::AtomicU64 =
872 std::sync::atomic::AtomicU64::new(UNSAMPLED_AT_MS);
873#[cfg(any(target_os = "macos", target_os = "linux"))]
874static LAST_ALLOCATOR_SLACK_RELIEF_FREED_BYTES: std::sync::atomic::AtomicU64 =
875 std::sync::atomic::AtomicU64::new(0);
876
877#[cfg(test)]
878static ALLOCATOR_SNAPSHOT_THREADS: std::sync::Mutex<Vec<String>> =
879 std::sync::Mutex::new(Vec::new());
880
881#[cfg(test)]
882thread_local! {
883 static THREAD_ALLOCATOR_SNAPSHOT_CALLS: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
890}
891
892#[cfg(test)]
893pub(crate) fn allocator_snapshot_calls_for_test() -> u64 {
894 THREAD_ALLOCATOR_SNAPSHOT_CALLS.with(|calls| calls.get())
895}
896
897#[cfg(test)]
898fn allocator_observation_test_mutex() -> &'static std::sync::Mutex<()> {
899 static LOCK: std::sync::OnceLock<std::sync::Mutex<()>> = std::sync::OnceLock::new();
900 LOCK.get_or_init(|| std::sync::Mutex::new(()))
901}
902
903#[cfg(test)]
904pub(crate) fn allocator_observation_test_lock() -> std::sync::MutexGuard<'static, ()> {
905 allocator_observation_test_mutex()
906 .lock()
907 .unwrap_or_else(std::sync::PoisonError::into_inner)
908}
909
910#[cfg(test)]
911pub(crate) fn reset_allocator_observation_for_test() {
912 #[cfg(any(target_os = "macos", target_os = "linux"))]
913 {
914 ALLOCATOR_OBSERVATION_SEQUENCE.store(0, std::sync::atomic::Ordering::Release);
915 LAST_ALLOCATOR_OBSERVATION_STATUS.store(
916 ALLOCATOR_OBSERVATION_UNSAMPLED,
917 std::sync::atomic::Ordering::Release,
918 );
919 LAST_OBSERVED_ALLOCATOR_BYTES_IN_USE.store(
920 UNKNOWN_ALLOCATOR_SLACK,
921 std::sync::atomic::Ordering::Release,
922 );
923 LAST_OBSERVED_ALLOCATOR_SIZE_ALLOCATED.store(
924 UNKNOWN_ALLOCATOR_SLACK,
925 std::sync::atomic::Ordering::Release,
926 );
927 LAST_OBSERVED_ALLOCATOR_SLACK_BYTES.store(
928 UNKNOWN_ALLOCATOR_SLACK,
929 std::sync::atomic::Ordering::Release,
930 );
931 LAST_ALLOCATOR_OBSERVATION_AT_MS
932 .store(UNSAMPLED_AT_MS, std::sync::atomic::Ordering::Release);
933 LAST_ALLOCATOR_SLACK_SAMPLE_AT_MS
934 .store(UNSAMPLED_AT_MS, std::sync::atomic::Ordering::Release);
935 }
936}
937
938#[cfg(any(target_os = "macos", target_os = "linux"))]
939fn allocator_slack_elapsed_ms(now: std::time::Instant) -> u64 {
940 let origin = ALLOCATOR_SLACK_CACHE_ORIGIN.get_or_init(|| now);
941 now.duration_since(*origin)
942 .as_millis()
943 .min(u128::from(u64::MAX)) as u64
944}
945
946#[cfg(any(target_os = "macos", target_os = "linux"))]
947fn cached_allocator_slack() -> Option<u64> {
948 let slack = LAST_OBSERVED_ALLOCATOR_SLACK_BYTES.load(std::sync::atomic::Ordering::Acquire);
949 (slack != UNKNOWN_ALLOCATOR_SLACK).then_some(slack)
950}
951
952#[cfg(any(target_os = "macos", target_os = "linux"))]
953fn cached_allocator_not_estimated_reason() -> &'static str {
954 #[cfg(all(target_os = "linux", target_env = "gnu"))]
955 {
956 "mallinfo2_requires_glibc_2_33"
957 }
958 #[cfg(not(all(target_os = "linux", target_env = "gnu")))]
959 {
960 "platform_allocator_statistics_unavailable"
961 }
962}
963
964pub fn cached_allocator_observation() -> Option<(AllocatorMemorySnapshot, u64)> {
968 #[cfg(any(target_os = "macos", target_os = "linux"))]
969 {
970 loop {
971 let sequence =
972 ALLOCATOR_OBSERVATION_SEQUENCE.load(std::sync::atomic::Ordering::Acquire);
973 if sequence % 2 != 0 {
974 std::hint::spin_loop();
975 continue;
976 }
977 let sampled_at_ms =
978 LAST_ALLOCATOR_OBSERVATION_AT_MS.load(std::sync::atomic::Ordering::Acquire);
979 if sampled_at_ms == UNSAMPLED_AT_MS {
980 return None;
981 }
982 let status =
983 LAST_ALLOCATOR_OBSERVATION_STATUS.load(std::sync::atomic::Ordering::Acquire);
984 let bytes_in_use =
985 LAST_OBSERVED_ALLOCATOR_BYTES_IN_USE.load(std::sync::atomic::Ordering::Acquire);
986 let size_allocated =
987 LAST_OBSERVED_ALLOCATOR_SIZE_ALLOCATED.load(std::sync::atomic::Ordering::Acquire);
988 let retained_slack =
989 LAST_OBSERVED_ALLOCATOR_SLACK_BYTES.load(std::sync::atomic::Ordering::Acquire);
990 let sequence_after =
991 ALLOCATOR_OBSERVATION_SEQUENCE.load(std::sync::atomic::Ordering::Acquire);
992 if sequence != sequence_after || sequence_after % 2 != 0 {
993 continue;
994 }
995 let snapshot = match status {
996 ALLOCATOR_OBSERVATION_MEASURED
997 if bytes_in_use != UNKNOWN_ALLOCATOR_SLACK
998 && size_allocated != UNKNOWN_ALLOCATOR_SLACK
999 && retained_slack != UNKNOWN_ALLOCATOR_SLACK =>
1000 {
1001 AllocatorMemorySnapshot {
1002 status: "measured",
1003 bytes_in_use: Some(bytes_in_use),
1004 size_allocated: Some(size_allocated),
1005 retained_slack_bytes: Some(retained_slack),
1006 not_estimated: None,
1007 }
1008 }
1009 ALLOCATOR_OBSERVATION_NOT_ESTIMATED => {
1010 AllocatorMemorySnapshot::not_estimated(cached_allocator_not_estimated_reason())
1011 }
1012 _ => return None,
1013 };
1014 let age_ms =
1015 allocator_slack_elapsed_ms(std::time::Instant::now()).saturating_sub(sampled_at_ms);
1016 return Some((snapshot, age_ms));
1017 }
1018 }
1019 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
1020 {
1021 None
1022 }
1023}
1024
1025#[cfg(any(target_os = "macos", target_os = "linux"))]
1026fn publish_allocator_slack(snapshot: &AllocatorMemorySnapshot, sampled_at_ms: u64) {
1027 let _publish_guard = ALLOCATOR_OBSERVATION_PUBLISH_LOCK
1028 .lock()
1029 .unwrap_or_else(std::sync::PoisonError::into_inner);
1030 ALLOCATOR_OBSERVATION_SEQUENCE.fetch_add(1, std::sync::atomic::Ordering::AcqRel);
1031 LAST_ALLOCATOR_OBSERVATION_STATUS.store(
1032 if snapshot.status == "measured" {
1033 ALLOCATOR_OBSERVATION_MEASURED
1034 } else {
1035 ALLOCATOR_OBSERVATION_NOT_ESTIMATED
1036 },
1037 std::sync::atomic::Ordering::Relaxed,
1038 );
1039 LAST_OBSERVED_ALLOCATOR_BYTES_IN_USE.store(
1040 snapshot.bytes_in_use.unwrap_or(UNKNOWN_ALLOCATOR_SLACK),
1041 std::sync::atomic::Ordering::Relaxed,
1042 );
1043 LAST_OBSERVED_ALLOCATOR_SIZE_ALLOCATED.store(
1044 snapshot.size_allocated.unwrap_or(UNKNOWN_ALLOCATOR_SLACK),
1045 std::sync::atomic::Ordering::Relaxed,
1046 );
1047 LAST_OBSERVED_ALLOCATOR_SLACK_BYTES.store(
1048 snapshot
1049 .retained_slack_bytes
1050 .unwrap_or(UNKNOWN_ALLOCATOR_SLACK),
1051 std::sync::atomic::Ordering::Relaxed,
1052 );
1053 LAST_ALLOCATOR_OBSERVATION_AT_MS.store(sampled_at_ms, std::sync::atomic::Ordering::Relaxed);
1054 ALLOCATOR_OBSERVATION_SEQUENCE.fetch_add(1, std::sync::atomic::Ordering::Release);
1055 LAST_ALLOCATOR_SLACK_SAMPLE_AT_MS.store(sampled_at_ms, std::sync::atomic::Ordering::Release);
1056}
1057
1058#[cfg(any(target_os = "macos", target_os = "linux"))]
1059fn sample_is_stale(now_ms: u64) -> bool {
1060 let sampled_at = LAST_ALLOCATOR_SLACK_SAMPLE_AT_MS.load(std::sync::atomic::Ordering::Acquire);
1061 sampled_at == UNSAMPLED_AT_MS
1062 || now_ms.saturating_sub(sampled_at) >= ALLOCATOR_SLACK_SAMPLE_INTERVAL.as_millis() as u64
1063}
1064
1065#[cfg(any(target_os = "macos", target_os = "linux"))]
1066fn reserve_sample(now_ms: u64) -> Option<u64> {
1067 let previous = LAST_ALLOCATOR_SLACK_SAMPLE_AT_MS.load(std::sync::atomic::Ordering::Acquire);
1068 if !sample_is_stale(now_ms) {
1069 return None;
1070 }
1071 LAST_ALLOCATOR_SLACK_SAMPLE_AT_MS
1072 .compare_exchange(
1073 previous,
1074 now_ms,
1075 std::sync::atomic::Ordering::AcqRel,
1076 std::sync::atomic::Ordering::Acquire,
1077 )
1078 .ok()
1079}
1080
1081#[cfg(any(target_os = "macos", target_os = "linux"))]
1082fn reserve_relief(now_ms: u64) -> Option<u64> {
1083 let previous = LAST_ALLOCATOR_SLACK_RELIEF_AT_MS.load(std::sync::atomic::Ordering::Acquire);
1084 let due = cached_allocator_slack().is_some_and(|slack| {
1085 slack >= ALLOCATOR_SLACK_RELIEF_THRESHOLD_BYTES
1086 && (previous == UNSAMPLED_AT_MS
1087 || now_ms.saturating_sub(previous)
1088 >= ALLOCATOR_SLACK_RELIEF_MIN_INTERVAL.as_millis() as u64)
1089 });
1090 due.then(|| {
1091 LAST_ALLOCATOR_SLACK_RELIEF_AT_MS
1092 .compare_exchange(
1093 previous,
1094 now_ms,
1095 std::sync::atomic::Ordering::AcqRel,
1096 std::sync::atomic::Ordering::Acquire,
1097 )
1098 .ok()
1099 })?
1100}
1101
1102#[cfg(any(target_os = "macos", target_os = "linux"))]
1103fn log_allocator_relief(relief: AllocatorPressureRelief) {
1104 log::info!(
1105 "allocator slack relief: released={} allocator_slack_bytes_before={:?} allocator_slack_bytes_after={:?} rss_bytes_before={:?} rss_bytes_after={:?}",
1106 relief.bytes_released,
1107 relief.allocator_before.retained_slack_bytes,
1108 relief.allocator_after.retained_slack_bytes,
1109 relief.rss_before_bytes,
1110 relief.rss_after_bytes,
1111 );
1112}
1113
1114pub fn last_allocator_relief_at_ms() -> Option<u64> {
1115 #[cfg(any(target_os = "macos", target_os = "linux"))]
1116 {
1117 let value = LAST_ALLOCATOR_SLACK_RELIEF_AT_MS.load(std::sync::atomic::Ordering::Acquire);
1118 return (value != UNSAMPLED_AT_MS).then_some(value);
1119 }
1120 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
1121 {
1122 None
1123 }
1124}
1125
1126pub fn last_allocator_relief_freed_bytes() -> u64 {
1127 #[cfg(any(target_os = "macos", target_os = "linux"))]
1128 {
1129 return LAST_ALLOCATOR_SLACK_RELIEF_FREED_BYTES.load(std::sync::atomic::Ordering::Acquire);
1130 }
1131 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
1132 {
1133 0
1134 }
1135}
1136
1137#[cfg(any(target_os = "macos", target_os = "linux"))]
1139pub fn allocator_slack_relief_due(
1140 retained_slack_bytes: Option<u64>,
1141 last_relief: Option<std::time::Instant>,
1142 now: std::time::Instant,
1143) -> bool {
1144 let Some(slack) = retained_slack_bytes else {
1145 return false;
1146 };
1147 if slack < ALLOCATOR_SLACK_RELIEF_THRESHOLD_BYTES {
1148 return false;
1149 }
1150 match last_relief {
1151 None => true,
1152 Some(at) => now.duration_since(at) >= ALLOCATOR_SLACK_RELIEF_MIN_INTERVAL,
1153 }
1154}
1155
1156#[cfg(any(target_os = "macos", target_os = "linux"))]
1166pub fn spawn_allocator_slack_relief_if_due(now: std::time::Instant) -> bool {
1167 let now_ms = allocator_slack_elapsed_ms(now);
1168 if sample_is_stale(now_ms) {
1169 let Some(previous_sample) = reserve_sample(now_ms) else {
1170 return false;
1171 };
1172 let spawned = std::thread::Builder::new()
1173 .name("aft-mem-slack-sample".to_string())
1174 .spawn(move || {
1175 let before = allocator_memory_snapshot();
1176 publish_allocator_slack(&before, now_ms);
1177 if reserve_relief(now_ms).is_some() {
1178 let relief = relieve_allocator_pressure_from_snapshot(before);
1179 publish_allocator_slack(
1180 &relief.allocator_after,
1181 allocator_slack_elapsed_ms(std::time::Instant::now()),
1182 );
1183 LAST_ALLOCATOR_SLACK_RELIEF_FREED_BYTES
1184 .store(relief.bytes_released, std::sync::atomic::Ordering::Release);
1185 log_allocator_relief(relief);
1186 }
1187 })
1188 .is_ok();
1189 if !spawned {
1190 let _ = LAST_ALLOCATOR_SLACK_SAMPLE_AT_MS.compare_exchange(
1191 now_ms,
1192 previous_sample,
1193 std::sync::atomic::Ordering::AcqRel,
1194 std::sync::atomic::Ordering::Acquire,
1195 );
1196 }
1197 return false;
1198 }
1199
1200 let Some(previous_relief) = reserve_relief(now_ms) else {
1201 return false;
1202 };
1203 let spawned = std::thread::Builder::new()
1204 .name("aft-mem-relief".to_string())
1205 .spawn(move || {
1206 let relief = relieve_allocator_pressure();
1207 publish_allocator_slack(
1208 &relief.allocator_after,
1209 allocator_slack_elapsed_ms(std::time::Instant::now()),
1210 );
1211 LAST_ALLOCATOR_SLACK_RELIEF_FREED_BYTES
1212 .store(relief.bytes_released, std::sync::atomic::Ordering::Release);
1213 log_allocator_relief(relief);
1214 })
1215 .is_ok();
1216 if !spawned {
1217 let _ = LAST_ALLOCATOR_SLACK_RELIEF_AT_MS.compare_exchange(
1218 now_ms,
1219 previous_relief,
1220 std::sync::atomic::Ordering::AcqRel,
1221 std::sync::atomic::Ordering::Acquire,
1222 );
1223 }
1224 spawned
1225}
1226
1227#[cfg(not(any(target_os = "macos", target_os = "linux")))]
1230pub fn spawn_allocator_slack_relief_if_due(_now: std::time::Instant) -> bool {
1231 false
1232}
1233
1234#[cfg(target_os = "macos")]
1239fn relieve_allocator_pressure_from_snapshot(
1240 allocator_before: AllocatorMemorySnapshot,
1241) -> AllocatorPressureRelief {
1242 let rss_before_bytes = process_rss_bytes();
1243 let bytes_released = unsafe { malloc_zone_pressure_relief(std::ptr::null_mut(), 0) };
1244 let allocator_after = allocator_memory_snapshot();
1245 let rss_after_bytes = process_rss_bytes();
1246 AllocatorPressureRelief {
1247 bytes_released: usize_to_u64(bytes_released),
1248 rss_before_bytes,
1249 rss_after_bytes,
1250 allocator_before,
1251 allocator_after,
1252 }
1253}
1254
1255#[cfg(target_os = "macos")]
1256pub fn relieve_allocator_pressure() -> AllocatorPressureRelief {
1257 relieve_allocator_pressure_from_snapshot(allocator_memory_snapshot())
1258}
1259
1260#[cfg(target_os = "linux")]
1261fn relieve_allocator_pressure_from_snapshot(
1262 allocator_before: AllocatorMemorySnapshot,
1263) -> AllocatorPressureRelief {
1264 let rss_before_bytes = process_rss_bytes();
1265 #[cfg(target_env = "gnu")]
1266 if let Some(malloc_trim) = resolved_malloc_trim() {
1267 unsafe { malloc_trim(0) };
1270 }
1271 let allocator_after = allocator_memory_snapshot();
1272 let rss_after_bytes = process_rss_bytes();
1273 let bytes_released = allocator_before
1274 .size_allocated
1275 .zip(allocator_after.size_allocated)
1276 .map(|(before, after)| before.saturating_sub(after))
1277 .unwrap_or(0);
1278 AllocatorPressureRelief {
1279 bytes_released,
1280 rss_before_bytes,
1281 rss_after_bytes,
1282 allocator_before,
1283 allocator_after,
1284 }
1285}
1286
1287#[cfg(target_os = "linux")]
1288pub fn relieve_allocator_pressure() -> AllocatorPressureRelief {
1289 relieve_allocator_pressure_from_snapshot(allocator_memory_snapshot())
1290}
1291
1292#[cfg(target_os = "macos")]
1293fn process_rss_bytes() -> Option<u64> {
1294 let mut info = std::mem::MaybeUninit::<libc::proc_taskinfo>::zeroed();
1295 let size = std::mem::size_of::<libc::proc_taskinfo>();
1296 let written = unsafe {
1297 libc::proc_pidinfo(
1298 libc::getpid(),
1299 libc::PROC_PIDTASKINFO,
1300 0,
1301 info.as_mut_ptr().cast(),
1302 i32::try_from(size).ok()?,
1303 )
1304 };
1305 if written != i32::try_from(size).ok()? {
1306 return None;
1307 }
1308 Some(unsafe { info.assume_init() }.pti_resident_size)
1309}
1310
1311#[cfg(target_os = "macos")]
1314fn process_phys_footprint_bytes() -> Option<u64> {
1315 let mut usage = std::mem::MaybeUninit::<libc::rusage_info_v4>::zeroed();
1316 let rc = unsafe {
1317 libc::proc_pid_rusage(
1318 libc::getpid(),
1319 libc::RUSAGE_INFO_V4,
1320 usage.as_mut_ptr().cast(),
1321 )
1322 };
1323 if rc != 0 {
1324 return None;
1325 }
1326 Some(unsafe { usage.assume_init() }.ri_phys_footprint)
1327}
1328
1329#[cfg(not(target_os = "macos"))]
1330fn process_phys_footprint_bytes() -> Option<u64> {
1331 None
1332}
1333
1334#[cfg(target_os = "linux")]
1335fn process_rss_bytes() -> Option<u64> {
1336 let statm = std::fs::read_to_string("/proc/self/statm").ok()?;
1337 let resident_pages = statm.split_whitespace().nth(1)?.parse::<u64>().ok()?;
1338 let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
1339 if page_size <= 0 {
1340 return None;
1341 }
1342 resident_pages.checked_mul(page_size as u64)
1343}
1344
1345#[cfg(not(any(target_os = "macos", target_os = "linux")))]
1346fn process_rss_bytes() -> Option<u64> {
1347 None
1348}
1349
1350pub(crate) fn rss_bytes() -> Option<u64> {
1351 process_rss_bytes()
1352}
1353
1354#[cfg(test)]
1355mod tests {
1356 use super::*;
1357
1358 #[test]
1359 fn process_snapshot_preserves_negative_residuals() {
1360 assert_eq!(signed_difference(5, 8), -3);
1361 }
1362
1363 #[cfg(any(target_os = "macos", target_os = "linux"))]
1364 #[test]
1365 fn loop_slack_decision_never_reads_allocator_statistics() {
1366 LAST_OBSERVED_ALLOCATOR_SLACK_BYTES.store(
1367 UNKNOWN_ALLOCATOR_SLACK,
1368 std::sync::atomic::Ordering::Release,
1369 );
1370 LAST_ALLOCATOR_SLACK_SAMPLE_AT_MS
1371 .store(UNSAMPLED_AT_MS, std::sync::atomic::Ordering::Release);
1372 LAST_ALLOCATOR_SLACK_RELIEF_AT_MS
1373 .store(UNSAMPLED_AT_MS, std::sync::atomic::Ordering::Release);
1374 ALLOCATOR_SNAPSHOT_THREADS
1375 .lock()
1376 .expect("clear allocator snapshot thread log")
1377 .clear();
1378
1379 for _ in 0..1_000 {
1380 assert!(!spawn_allocator_slack_relief_if_due(
1381 std::time::Instant::now()
1382 ));
1383 }
1384 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
1385 let sampler_ran = || {
1386 ALLOCATOR_SNAPSHOT_THREADS
1387 .lock()
1388 .expect("read allocator snapshot thread log")
1389 .iter()
1390 .any(|name| name == "aft-mem-slack-sample")
1391 };
1392 while !sampler_ran() {
1393 assert!(std::time::Instant::now() < deadline, "sampler did not run");
1394 std::thread::yield_now();
1395 }
1396 let loop_thread = std::thread::current()
1400 .name()
1401 .unwrap_or("unnamed")
1402 .to_string();
1403 let threads = ALLOCATOR_SNAPSHOT_THREADS
1404 .lock()
1405 .expect("read allocator snapshot thread log")
1406 .clone();
1407 assert!(
1408 !threads.iter().any(|name| *name == loop_thread),
1409 "the loop-side decision must never read allocator statistics: {threads:?}"
1410 );
1411 }
1412
1413 #[cfg(any(target_os = "macos", target_os = "linux"))]
1414 #[test]
1415 fn slack_relief_fires_on_large_slack_and_respects_spacing() {
1416 use std::time::{Duration, Instant};
1417 let now = Instant::now();
1418 let big = Some(ALLOCATOR_SLACK_RELIEF_THRESHOLD_BYTES);
1419 assert!(!allocator_slack_relief_due(None, None, now));
1421 assert!(!allocator_slack_relief_due(
1423 Some(ALLOCATOR_SLACK_RELIEF_THRESHOLD_BYTES - 1),
1424 None,
1425 now
1426 ));
1427 assert!(allocator_slack_relief_due(big, None, now));
1429 let recent = now - Duration::from_secs(10);
1431 assert!(!allocator_slack_relief_due(big, Some(recent), now));
1432 let stale = now - ALLOCATOR_SLACK_RELIEF_MIN_INTERVAL;
1434 assert!(allocator_slack_relief_due(big, Some(stale), now));
1435 }
1436
1437 #[test]
1438 fn json_estimator_scales_with_payload_content() {
1439 let empty = estimated_json_bytes(&serde_json::json!({}));
1440 let populated = estimated_json_bytes(&serde_json::json!({"message": "hello"}));
1441 assert_eq!(empty, 0);
1442 assert!(populated >= 12);
1443 }
1444
1445 #[test]
1446 fn process_snapshot_exposes_sqlite_and_allocator_sections() {
1447 let shared = MemoryEstimate::estimated(7);
1448 let snapshot = ProcessMemorySnapshot::from_roots(&BTreeMap::new(), &shared);
1449 assert_eq!(snapshot.sqlite.status, "measured");
1450 assert!(snapshot.sqlite.memory_highwater_bytes >= snapshot.sqlite.memory_used_bytes);
1451 assert_eq!(
1452 snapshot.total_attributed_bytes,
1453 snapshot.sqlite.memory_used_bytes.saturating_add(7)
1454 );
1455
1456 let serialized = serde_json::to_value(&snapshot).expect("serialize process memory");
1457 assert!(serialized["sqlite"]["memory_used_bytes"].is_u64());
1458 assert!(serialized["allocator"].get("bytes_in_use").is_some());
1459 assert!(serialized["allocator"].get("size_allocated").is_some());
1460 assert!(serialized["allocator"]
1461 .get("retained_slack_bytes")
1462 .is_some());
1463 }
1464
1465 #[cfg(any(target_os = "macos", all(target_os = "linux", target_env = "gnu")))]
1466 #[test]
1467 fn allocator_snapshot_reports_measured_slack() {
1468 let allocator = allocator_memory_snapshot();
1469 if allocator.status == "measured" {
1470 let in_use = allocator.bytes_in_use.expect("allocator bytes in use");
1471 let allocated = allocator.size_allocated.expect("allocator size allocated");
1472 assert_eq!(
1473 allocator.retained_slack_bytes,
1474 Some(allocated.saturating_sub(in_use))
1475 );
1476 } else {
1477 assert_eq!(allocator.status, "not_estimated_on_this_platform");
1478 assert_eq!(allocator.bytes_in_use, None);
1479 assert_eq!(allocator.size_allocated, None);
1480 assert_eq!(allocator.retained_slack_bytes, None);
1481 assert_eq!(
1482 allocator.not_estimated,
1483 Some("mallinfo2_requires_glibc_2_33")
1484 );
1485 }
1486 }
1487
1488 #[cfg(target_os = "linux")]
1489 #[test]
1490 fn linux_allocator_pressure_relief_smoke() {
1491 let mut allocation = vec![0u8; 32 * 1024 * 1024];
1492 for byte in allocation.iter_mut().step_by(4096) {
1493 *byte = 1;
1494 }
1495 std::hint::black_box(&allocation);
1496 drop(allocation);
1497
1498 let relief = relieve_allocator_pressure();
1499 std::hint::black_box(relief);
1500
1501 #[cfg(target_env = "gnu")]
1502 assert!(
1503 resolved_malloc_trim().is_some(),
1504 "glibc malloc_trim must be available for the Linux relief path"
1505 );
1506 }
1507
1508 #[cfg(not(any(target_os = "macos", all(target_os = "linux", target_env = "gnu"))))]
1509 #[test]
1510 fn allocator_snapshot_is_honest_when_platform_counters_are_unavailable() {
1511 let allocator = allocator_memory_snapshot();
1512 assert_eq!(allocator.status, "not_estimated_on_this_platform");
1513 assert_eq!(allocator.bytes_in_use, None);
1514 assert_eq!(allocator.size_allocated, None);
1515 assert_eq!(allocator.retained_slack_bytes, None);
1516 assert_eq!(
1517 allocator.not_estimated,
1518 Some("platform_allocator_statistics_unavailable")
1519 );
1520 }
1521
1522 #[cfg(target_os = "macos")]
1523 #[test]
1524 #[ignore = "bounded live RSS experiment; run explicitly after allocator changes"]
1525 fn allocator_pressure_relief_warm_then_idle_measurement() {
1526 let warm_pages = (0..16 * 1024)
1527 .map(|seed| {
1528 let mut page = Box::new([0u8; 4096]);
1529 page[0] = seed as u8;
1530 page
1531 })
1532 .collect::<Vec<_>>();
1533 std::hint::black_box(&warm_pages);
1534 drop(warm_pages);
1535
1536 let relief = relieve_allocator_pressure();
1537 let sqlite = SqliteMemorySnapshot::measure();
1538 eprintln!(
1539 "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={}",
1540 relief.rss_before_bytes,
1541 relief.rss_after_bytes,
1542 relief.allocator_before.bytes_in_use,
1543 relief.allocator_after.bytes_in_use,
1544 relief.allocator_before.size_allocated,
1545 relief.allocator_after.size_allocated,
1546 relief.allocator_before.retained_slack_bytes,
1547 relief.allocator_after.retained_slack_bytes,
1548 relief.bytes_released,
1549 sqlite.memory_used_bytes,
1550 sqlite.memory_highwater_bytes,
1551 );
1552 assert_eq!(relief.allocator_before.status, "measured");
1553 assert_eq!(relief.allocator_after.status, "measured");
1554 }
1555}