1use crate::tracking::TrackingAllocator;
7use anyhow::Context;
8#[cfg(target_os = "linux")]
9use cgroups_rs::cgroup::{get_cgroups_relative_paths, Cgroup, UNIFIED_MOUNTPOINT};
10#[cfg(target_os = "linux")]
11use cgroups_rs::hierarchies::{V1, V2};
12#[cfg(target_os = "linux")]
13use cgroups_rs::memory::MemController;
14#[cfg(target_os = "linux")]
15use cgroups_rs::{Hierarchy, MaxValue};
16use kumo_prometheus::declare_metric;
17use nix::sys::resource::{rlim_t, RLIM_INFINITY};
18use nix::unistd::{sysconf, SysconfVar};
19#[cfg(any(target_os = "linux", test))]
20use std::collections::HashMap;
21use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
22use std::sync::{LazyLock, Mutex};
23use std::time::Duration;
24use tikv_jemallocator::Jemalloc;
25use tokio::sync::watch::Receiver;
26
27pub mod tracking;
28
29pub use tracking::{set_tracking_callstacks, tracking_stats};
30
31#[global_allocator]
32static GLOBAL: TrackingAllocator<Jemalloc> = TrackingAllocator::new(Jemalloc);
33
34declare_metric! {
35static LOW_COUNT: IntCounter("memory_low_count");
37}
38
39declare_metric! {
40static OVER_LIMIT_COUNT: IntCounter("memory_over_limit_count");
42}
43
44declare_metric! {
45static MEM_USAGE: Gauge("memory_usage");
50}
51
52declare_metric! {
53static MEM_LIMIT: Gauge("memory_limit");
55}
56
57declare_metric! {
58static MEM_COUNTED: Gauge("memory_usage_rust");
60}
61
62declare_metric! {
63static LOW_MEM_THRESH: Gauge("memory_low_thresh");
65}
66
67static SUBSCRIBER: LazyLock<Mutex<Option<Receiver<()>>>> = LazyLock::new(|| Mutex::new(None));
68
69static OVER_LIMIT: AtomicBool = AtomicBool::new(false);
70static LOW_MEM: AtomicBool = AtomicBool::new(false);
71
72#[cfg(target_os = "linux")]
75static WARNED_EMPTY_MEMORY_STAT: AtomicBool = AtomicBool::new(false);
76
77static HEAD_ROOM: AtomicUsize = AtomicUsize::new(u32::MAX as usize);
84
85#[derive(Debug, Clone, Copy)]
91pub struct MemoryUsage {
92 pub bytes: u64,
93}
94
95impl std::fmt::Display for MemoryUsage {
96 fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
97 write!(fmt, "{}", human(self.bytes))
98 }
99}
100
101impl MemoryUsage {
102 pub fn get() -> anyhow::Result<Self> {
103 #[cfg(target_os = "linux")]
104 {
105 if let Ok(v2) = Self::get_cgroup(true) {
106 return Ok(v2);
107 }
108 if let Ok(v1) = Self::get_cgroup(false) {
109 return Ok(v1);
110 }
111 }
112 Self::get_linux_statm()
113 }
114
115 #[cfg(target_os = "linux")]
116 fn get_cgroup(v2: bool) -> anyhow::Result<Self> {
117 let cgroup = get_my_cgroup(v2)?;
118 let mem: &MemController = cgroup
119 .controller_of()
120 .ok_or_else(|| anyhow::anyhow!("no memory controller?"))?;
121 let stat = mem.memory_stat();
122 let current = stat.usage_in_bytes;
123
124 match parse_working_set_inputs(v2, &stat.stat.raw) {
125 Some((inactive_file, anon)) => Ok(Self {
126 bytes: working_set_usage(current, inactive_file, anon),
127 }),
128 None => {
129 if !WARNED_EMPTY_MEMORY_STAT.swap(true, Ordering::Relaxed) {
132 tracing::warn!(
133 "cgroup memory.stat was empty or unreadable; \
134 reporting raw memory.current ({}) as usage",
135 human(current)
136 );
137 }
138 Ok(Self { bytes: current })
139 }
140 }
141 }
142
143 pub fn get_linux_statm() -> anyhow::Result<Self> {
144 let data = std::fs::read_to_string("/proc/self/statm")?;
145 let fields: Vec<&str> = data.split(' ').collect();
146 let rss: u64 = fields[1].parse()?;
147 Ok(Self {
148 bytes: rss * sysconf(SysconfVar::PAGE_SIZE)?.unwrap_or(4 * 1024) as u64,
149 })
150 }
151}
152
153#[cfg(any(target_os = "linux", test))]
160fn working_set_usage(current: u64, inactive_file: u64, anon: u64) -> u64 {
161 current.saturating_sub(inactive_file).max(anon)
162}
163
164#[cfg(any(target_os = "linux", test))]
174fn parse_working_set_inputs(v2: bool, raw: &HashMap<String, u64>) -> Option<(u64, u64)> {
175 if raw.is_empty() {
176 return None;
177 }
178 let get = |key: &str| raw.get(key).copied().unwrap_or(0);
179 if v2 {
180 Some((get("inactive_file"), get("anon")))
181 } else {
182 let anon = get("total_rss").saturating_add(get("total_rss_huge"));
183 Some((get("total_inactive_file"), anon))
184 }
185}
186
187fn human(n: u64) -> String {
188 humansize::format_size(n, humansize::DECIMAL)
189}
190
191#[derive(Debug, Clone, Copy)]
193pub struct MemoryLimits {
194 pub soft_limit: Option<u64>,
195 pub hard_limit: Option<u64>,
196}
197
198impl std::fmt::Display for MemoryLimits {
199 fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
200 let soft = self.soft_limit.map(human);
201 let hard = self.hard_limit.map(human);
202 write!(fmt, "soft={soft:?}, hard={hard:?}")
203 }
204}
205
206impl MemoryLimits {
207 pub fn min(self, other: Self) -> Self {
208 Self {
209 soft_limit: min_opt_limit(self.soft_limit, other.soft_limit),
210 hard_limit: min_opt_limit(self.hard_limit, other.hard_limit),
211 }
212 }
213
214 pub fn is_unlimited(&self) -> bool {
215 self.soft_limit.is_none() && self.hard_limit.is_none()
216 }
217}
218
219fn rlim_to_opt(rlim: rlim_t) -> Option<u64> {
220 if rlim == RLIM_INFINITY {
221 None
222 } else {
223 Some(rlim)
224 }
225}
226
227#[cfg(target_os = "linux")]
228fn max_value_to_opt(value: Option<MaxValue>) -> anyhow::Result<Option<u64>> {
229 Ok(match value {
230 None | Some(MaxValue::Max) => None,
231 Some(MaxValue::Value(n)) if n >= 0 => Some(n as u64),
232 Some(MaxValue::Value(n)) => anyhow::bail!("unexpected negative limit {n}"),
233 })
234}
235
236fn min_opt_limit(a: Option<u64>, b: Option<u64>) -> Option<u64> {
237 match (a, b) {
238 (Some(a), Some(b)) => Some(a.min(b)),
239 (Some(a), None) | (None, Some(a)) => Some(a),
240 (None, None) => None,
241 }
242}
243
244impl MemoryLimits {
245 pub fn get_rlimits() -> anyhow::Result<Self> {
246 #[cfg(not(target_os = "macos"))]
247 let (rss_soft, rss_hard) =
248 nix::sys::resource::getrlimit(nix::sys::resource::Resource::RLIMIT_RSS)?;
249 #[cfg(target_os = "macos")]
250 let (rss_soft, rss_hard) = (RLIM_INFINITY, RLIM_INFINITY);
251
252 let soft_limit = rlim_to_opt(rss_soft);
253 let hard_limit = rlim_to_opt(rss_hard);
254
255 Ok(Self {
256 soft_limit,
257 hard_limit,
258 })
259 }
260
261 #[cfg(target_os = "linux")]
262 fn get_any_cgroup() -> anyhow::Result<Self> {
263 if let Ok(cg) = Self::get_cgroup(true) {
264 return Ok(cg);
265 }
266 Self::get_cgroup(false)
267 }
268
269 #[cfg(target_os = "linux")]
270 pub fn get_cgroup(v2: bool) -> anyhow::Result<Self> {
271 let cgroup = get_my_cgroup(v2)?;
272 let mem: &MemController = cgroup
273 .controller_of()
274 .ok_or_else(|| anyhow::anyhow!("no memory controller?"))?;
275
276 let limits = mem.get_mem()?;
277 Ok(Self {
278 soft_limit: max_value_to_opt(limits.high)?,
279 hard_limit: max_value_to_opt(limits.max)?,
280 })
281 }
282}
283
284#[cfg(target_os = "linux")]
287fn get_physical_memory() -> anyhow::Result<u64> {
288 let data = std::fs::read_to_string("/proc/meminfo")?;
289 for line in data.lines() {
290 if line.starts_with("MemTotal:") {
291 let mut iter = line.rsplit(' ');
292 let unit = iter
293 .next()
294 .ok_or_else(|| anyhow::anyhow!("expected unit"))?;
295 if unit != "kB" {
296 anyhow::bail!("unsupported /proc/meminfo unit {unit}");
297 }
298 let value = iter
299 .next()
300 .ok_or_else(|| anyhow::anyhow!("expected value"))?;
301 let value: u64 = value.parse()?;
302
303 return Ok(value * 1024);
304 }
305 }
306 anyhow::bail!("MemTotal not found in /proc/meminfo");
307}
308
309#[cfg(target_os = "linux")]
321fn get_usage_and_limit_impl() -> anyhow::Result<(MemoryUsage, MemoryLimits)> {
322 let mut limit = MemoryLimits::get_rlimits()?;
323 let mut usage = MemoryUsage::get_linux_statm()?;
324
325 if let Ok(cg_lim) = MemoryLimits::get_any_cgroup() {
326 if !cg_lim.is_unlimited() {
327 limit = limit.min(cg_lim);
328 usage = MemoryUsage::get()?;
329 }
330 }
331
332 if limit.hard_limit.is_none() {
333 let phys = get_physical_memory()?;
334 limit.hard_limit.replace(phys);
335 }
336 if limit.soft_limit.is_none() {
337 limit.soft_limit = limit.hard_limit.map(|lim| lim * 3 / 4);
338 }
339
340 Ok((usage, limit))
341}
342
343#[cfg(not(target_os = "linux"))]
344fn get_usage_and_limit_impl() -> anyhow::Result<(MemoryUsage, MemoryLimits)> {
345 Ok((
346 MemoryUsage { bytes: 0 },
347 MemoryLimits {
348 soft_limit: None,
349 hard_limit: None,
350 },
351 ))
352}
353
354static USER_HARD_LIMIT: AtomicUsize = AtomicUsize::new(0);
355static USER_SOFT_LIMIT: AtomicUsize = AtomicUsize::new(0);
356static USER_LOW_THRESH: AtomicUsize = AtomicUsize::new(0);
357
358pub fn set_hard_limit(limit: usize) {
359 USER_HARD_LIMIT.store(limit, Ordering::Relaxed);
360}
361
362pub fn set_soft_limit(limit: usize) {
363 USER_SOFT_LIMIT.store(limit, Ordering::Relaxed);
364}
365
366pub fn set_low_memory_thresh(limit: usize) {
367 USER_LOW_THRESH.store(limit, Ordering::Relaxed);
368}
369
370pub fn get_hard_limit() -> Option<u64> {
371 let user_limit = USER_HARD_LIMIT.load(Ordering::Relaxed);
372 if user_limit > 0 {
373 return Some(user_limit as u64);
374 }
375
376 let result = get_usage_and_limit()
377 .ok()
378 .and_then(|(_, limits)| limits.hard_limit);
379 result
380}
381
382pub fn get_soft_limit() -> Option<u64> {
383 let user_limit = USER_SOFT_LIMIT.load(Ordering::Relaxed);
384 if user_limit > 0 {
385 return Some(user_limit as u64);
386 }
387
388 get_usage_and_limit()
389 .ok()
390 .and_then(|(_, limits)| limits.soft_limit)
391}
392
393pub fn get_low_memory_thresh() -> Option<u64> {
394 let user_thresh = USER_LOW_THRESH.load(Ordering::Relaxed);
395 if user_thresh > 0 {
396 return Some(user_thresh as u64);
397 }
398
399 get_usage_and_limit()
400 .ok()
401 .and_then(|(_, limits)| limits.soft_limit)
402 .map(|limit| limit * 8 / 10)
403}
404
405pub fn get_usage_and_limit() -> anyhow::Result<(MemoryUsage, MemoryLimits)> {
406 let (usage, mut limit) = get_usage_and_limit_impl()?;
407
408 if let Ok(value) = std::env::var("KUMOD_MEMORY_HARD_LIMIT") {
409 limit.hard_limit.replace(
410 value
411 .parse()
412 .context("failed to parse KUMOD_MEMORY_HARD_LIMIT env var")?,
413 );
414 }
415 if let Ok(value) = std::env::var("KUMOD_MEMORY_SOFT_LIMIT") {
416 limit.soft_limit.replace(
417 value
418 .parse()
419 .context("failed to parse KUMOD_MEMORY_SOFT_LIMIT env var")?,
420 );
421 }
422
423 let hard = USER_HARD_LIMIT.load(Ordering::Relaxed);
424 if hard > 0 {
425 limit.hard_limit.replace(hard as u64);
426 }
427 let soft = USER_SOFT_LIMIT.load(Ordering::Relaxed);
428 if soft > 0 {
429 limit.soft_limit.replace(soft as u64);
430 }
431
432 Ok((usage, limit))
433}
434
435pub fn purge_thread_cache() {
439 unsafe {
440 tikv_jemalloc_sys::mallctl(
441 b"thread.tcache.flush\0".as_ptr() as *const _,
442 std::ptr::null_mut(),
443 std::ptr::null_mut(),
444 std::ptr::null_mut(),
445 0,
446 );
447 }
448}
449
450fn purge_all_arenas() {
453 unsafe {
454 tikv_jemalloc_sys::mallctl(
457 b"arena.4096.purge\0".as_ptr() as *const _,
458 std::ptr::null_mut(),
459 std::ptr::null_mut(),
460 std::ptr::null_mut(),
461 0,
462 );
463 }
464}
465
466fn dump_heap_profile() {
470 unsafe {
471 tikv_jemalloc_sys::mallctl(
472 b"prof.dump\0".as_ptr() as *const _,
473 std::ptr::null_mut(),
474 std::ptr::null_mut(),
475 std::ptr::null_mut(),
476 0,
477 );
478 }
479}
480
481fn memory_thread() {
484 let mut is_ok = true;
485 let mut is_low = false;
486
487 let (tx, rx) = tokio::sync::watch::channel(());
488 SUBSCRIBER.lock().unwrap().replace(rx);
489
490 loop {
491 MEM_COUNTED.set(crate::tracking::counted_usage() as f64);
492
493 match get_usage_and_limit() {
494 Ok((
495 MemoryUsage { bytes: usage },
496 MemoryLimits {
497 soft_limit: Some(limit),
498 hard_limit: _,
499 },
500 )) => {
501 let was_ok = is_ok;
502 is_ok = usage < limit;
503 OVER_LIMIT.store(is_ok, Ordering::SeqCst);
504 HEAD_ROOM.store(limit.saturating_sub(usage) as usize, Ordering::SeqCst);
505 MEM_USAGE.set(usage as f64);
506 MEM_LIMIT.set(limit as f64);
507
508 let mut low_thresh = USER_LOW_THRESH.load(Ordering::Relaxed) as u64;
509 if low_thresh == 0 {
510 low_thresh = limit * 8 / 10;
511 }
512 LOW_MEM_THRESH.set(low_thresh as f64);
513
514 let was_low = is_low;
515 is_low = usage > low_thresh;
516 LOW_MEM.store(is_low, Ordering::SeqCst);
517
518 if !was_low && is_low {
519 LOW_COUNT.inc();
521 }
522
523 if !is_ok && was_ok {
524 dump_heap_profile();
526 OVER_LIMIT_COUNT.inc();
527 tracing::error!(
528 "memory usage {} exceeds limit {}",
529 human(usage),
530 human(limit)
531 );
532 tx.send(()).ok();
533 purge_all_arenas();
534 } else if !was_ok && is_ok {
535 dump_heap_profile();
537 tracing::error!(
538 "memory usage {} is back within limit {}",
539 human(usage),
540 human(limit)
541 );
542 tx.send(()).ok();
543 } else {
544 if !is_ok {
545 purge_all_arenas();
546 }
547 tracing::debug!("memory usage {}, limit {}", human(usage), human(limit));
548 }
549 }
550 Ok((
551 MemoryUsage { bytes: 0 },
552 MemoryLimits {
553 soft_limit: None,
554 hard_limit: None,
555 },
556 )) => {
557 HEAD_ROOM.store(1024, Ordering::SeqCst);
560 }
561 Ok(_) => {}
562 Err(err) => tracing::error!("unable to query memory info: {err:#}"),
563 }
564
565 std::thread::sleep(Duration::from_secs(3));
566 }
567}
568
569pub fn get_headroom() -> usize {
572 HEAD_ROOM.load(Ordering::SeqCst)
573}
574
575pub fn low_memory() -> bool {
577 LOW_MEM.load(Ordering::SeqCst)
578}
579
580pub fn memory_status() -> MemoryStatus {
582 if get_headroom() == 0 {
583 MemoryStatus::NoMemory
584 } else if low_memory() {
585 MemoryStatus::LowMemory
586 } else {
587 MemoryStatus::Ok
588 }
589}
590
591#[derive(Copy, Clone, Debug, Eq, PartialEq)]
592pub enum MemoryStatus {
593 Ok,
594 LowMemory,
595 NoMemory,
596}
597
598pub fn subscribe_to_memory_status_changes() -> Option<Receiver<()>> {
601 SUBSCRIBER.lock().unwrap().clone()
602}
603
604pub async fn subscribe_to_memory_status_changes_async() -> Receiver<()> {
605 loop {
606 if let Some(rx) = subscribe_to_memory_status_changes() {
607 return rx;
608 }
609 tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
610 }
611}
612
613pub fn setup_memory_limit() -> anyhow::Result<()> {
615 let (usage, limit) = get_usage_and_limit()?;
616 tracing::debug!("usage: {usage:?}");
617 tracing::info!("using limits: {limit}");
618
619 std::thread::Builder::new()
620 .name("memory-monitor".to_string())
621 .spawn(memory_thread)?;
622
623 Ok(())
624}
625
626#[cfg(target_os = "linux")]
629fn get_my_cgroup(v2: bool) -> anyhow::Result<Cgroup> {
630 let paths = get_cgroups_relative_paths()?;
631 let h: Box<dyn Hierarchy> = if v2 {
632 Box::new(V2::new())
633 } else {
634 Box::new(V1::new())
635 };
636
637 let path = paths
638 .get("")
639 .ok_or_else(|| anyhow::anyhow!("couldn't resolve path"))?;
640
641 let cgroup = Cgroup::load(h, format!("{}/{}", UNIFIED_MOUNTPOINT, path));
642 Ok(cgroup)
643}
644
645#[derive(Copy, Clone)]
646pub struct NumBytes(pub usize);
647
648impl std::fmt::Debug for NumBytes {
649 fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
650 write!(
651 fmt,
652 "{} ({})",
653 self.0,
654 humansize::format_size(self.0, humansize::DECIMAL)
655 )
656 }
657}
658
659impl From<usize> for NumBytes {
660 fn from(n: usize) -> Self {
661 Self(n)
662 }
663}
664
665impl From<u64> for NumBytes {
666 fn from(n: u64) -> Self {
667 Self(n as usize)
668 }
669}
670
671#[derive(Copy, Clone)]
672pub struct Number(pub usize);
673
674impl std::fmt::Debug for Number {
675 fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
676 use num_format::{Locale, ToFormattedString};
677 write!(
678 fmt,
679 "{} ({})",
680 self.0,
681 self.0.to_formatted_string(&Locale::en)
682 )
683 }
684}
685
686impl From<usize> for Number {
687 fn from(n: usize) -> Self {
688 Self(n)
689 }
690}
691
692impl From<u64> for Number {
693 fn from(n: u64) -> Self {
694 Self(n as usize)
695 }
696}
697
698#[derive(Debug)]
699pub struct JemallocStats {
700 pub allocated: NumBytes,
702
703 pub active: NumBytes,
709
710 pub metadata: NumBytes,
713
714 pub resident: NumBytes,
717
718 pub mapped: NumBytes,
721
722 pub retained: NumBytes,
725}
726
727impl JemallocStats {
728 pub fn collect() -> Self {
729 use tikv_jemalloc_ctl::{epoch, stats};
730
731 epoch::advance().ok();
735
736 Self {
737 allocated: stats::allocated::read().unwrap_or(0).into(),
738 active: stats::active::read().unwrap_or(0).into(),
739 metadata: stats::metadata::read().unwrap_or(0).into(),
740 resident: stats::resident::read().unwrap_or(0).into(),
741 mapped: stats::mapped::read().unwrap_or(0).into(),
742 retained: stats::retained::read().unwrap_or(0).into(),
743 }
744 }
745}
746
747#[cfg(test)]
748mod tests {
749 use super::*;
750
751 const GB: u64 = 1024 * 1024 * 1024;
752 const MB: u64 = 1024 * 1024;
753
754 fn raw(pairs: &[(&str, u64)]) -> HashMap<String, u64> {
755 pairs
756 .iter()
757 .map(|(key, value)| ((*key).to_string(), *value))
758 .collect()
759 }
760
761 #[test]
762 fn working_set_normal() {
763 let current = 51 * GB;
766 let inactive_file = 46 * GB;
767 let anon = 1_800 * MB;
768 assert_eq!(
769 working_set_usage(current, inactive_file, anon),
770 current - inactive_file
771 );
772 }
773
774 #[test]
775 fn anon_floor_wins() {
776 let current = 5 * GB;
779 let inactive_file = 4 * GB;
780 let anon = 4 * GB;
781 assert_eq!(working_set_usage(current, inactive_file, anon), 4 * GB);
782 }
783
784 #[test]
785 fn underflow_no_panic() {
786 assert_eq!(working_set_usage(10 * GB, 10 * GB, 0), 0);
789 assert_eq!(working_set_usage(10 * GB, 12 * GB, 0), 0);
790 }
791
792 #[test]
793 fn parse_v2() {
794 let raw = raw(&[
797 ("anon", 2 * GB),
798 ("inactive_file", 3 * GB),
799 ("active_file", GB),
800 ("file", 4 * GB),
801 ]);
802 assert_eq!(parse_working_set_inputs(true, &raw), Some((3 * GB, 2 * GB)));
803 }
804
805 #[test]
806 fn parse_v1_hierarchical() {
807 let raw = raw(&[
810 ("total_rss", 2 * GB),
811 ("total_rss_huge", 512 * MB),
812 ("total_inactive_file", 3 * GB),
813 ("rss", GB),
814 ]);
815 assert_eq!(
816 parse_working_set_inputs(false, &raw),
817 Some((3 * GB, 2 * GB + 512 * MB))
818 );
819 }
820
821 #[test]
822 fn empty_raw_is_none() {
823 let raw = HashMap::new();
826 assert_eq!(parse_working_set_inputs(true, &raw), None);
827 assert_eq!(parse_working_set_inputs(false, &raw), None);
828 }
829
830 #[test]
831 fn missing_keys_default_zero() {
832 let raw = raw(&[("cache", GB)]);
835 assert_eq!(parse_working_set_inputs(true, &raw), Some((0, 0)));
836 }
837}