mz_clusterd/
usage_metrics.rs1use std::path::PathBuf;
16
17use serde::Serialize;
18use tracing::error;
19
20pub(crate) struct Collector {
22 pub disk_root: Option<PathBuf>,
23}
24
25impl Collector {
26 pub fn collect(&self) -> Usage {
28 let disk_bytes = self.collect_disk_usage();
29 let (memory_bytes, swap_bytes) = collect_heap_usage();
30 let heap_limit = collect_heap_limit();
31
32 Usage {
33 disk_bytes,
34 memory_bytes,
35 swap_bytes,
36 heap_limit,
37 }
38 }
39
40 fn collect_disk_usage(&self) -> Option<u64> {
41 let root = self.disk_root.as_deref()?;
42 match mz_metrics::usage::disk_usage(root) {
43 Ok(bytes) => Some(bytes),
44 Err(err) => {
45 error!("statvfs error: {err}");
46 None
47 }
48 }
49 }
50}
51
52#[derive(Serialize)]
54pub(crate) struct Usage {
55 disk_bytes: Option<u64>,
56 memory_bytes: Option<u64>,
57 swap_bytes: Option<u64>,
58 heap_limit: Option<u64>,
59}
60
61#[cfg(target_os = "linux")]
62mod linux {
63 use std::fs;
64 use std::path::Path;
65
66 use anyhow::{anyhow, bail};
67 use mz_compute::memory_limiter;
68 use mz_metrics::usage::ProcStatus;
69 use mz_ore::cast::CastInto;
70 use tracing::{debug, error};
71
72 pub fn collect_heap_usage() -> (Option<u64>, Option<u64>) {
74 use mz_ore::cast::CastInto;
75
76 match ProcStatus::from_proc() {
77 Ok(status) => {
78 let memory_bytes = status.vm_rss.cast_into();
79 let swap_bytes = status.vm_swap.cast_into();
80
81 debug!("memory usage: {memory_bytes}");
82 debug!("swap usage: {swap_bytes}");
83
84 (Some(memory_bytes), Some(swap_bytes))
85 }
86 Err(err) => {
87 error!("error reading /proc/self/status: {err}");
88 (None, None)
89 }
90 }
91 }
92
93 pub fn collect_heap_limit() -> Option<u64> {
95 let (phys_mem_limit, phys_swap_limit) = get_physical_limits()?;
97
98 let (cgroup_mem_limit, cgroup_swap_limit) = get_cgroup_limits();
100 let mem_limit = cgroup_mem_limit.unwrap_or(u64::MAX).min(phys_mem_limit);
101 let swap_limit = cgroup_swap_limit.unwrap_or(u64::MAX).min(phys_swap_limit);
102
103 let heap_limit = mem_limit + swap_limit;
104
105 let limiter_limit = memory_limiter::get_memory_limit().map(CastInto::cast_into);
107 let heap_limit = limiter_limit.unwrap_or(u64::MAX).min(heap_limit);
108
109 debug!("memory limit: {mem_limit} (phys={phys_mem_limit}, cgroup={cgroup_mem_limit:?})");
110 debug!("swap limit: {swap_limit} (phys={phys_swap_limit}, cgroup={cgroup_swap_limit:?})");
111 debug!("heap limit: {heap_limit} (limiter={limiter_limit:?})");
112
113 Some(heap_limit)
114 }
115
116 struct ProcMemInfo {
118 mem_total: u64,
119 swap_total: u64,
120 }
121
122 impl ProcMemInfo {
123 fn from_proc() -> anyhow::Result<Self> {
124 let contents = fs::read_to_string("/proc/meminfo")?;
125
126 fn parse_kib_line(line: &str) -> anyhow::Result<u64> {
127 if let Some(kib) = line
128 .split_whitespace()
129 .nth(1)
130 .and_then(|x| x.parse::<u64>().ok())
131 {
132 Ok(kib * 1024)
133 } else {
134 bail!("invalid meminfo line: {line}");
135 }
136 }
137
138 let mut memory = None;
139 let mut swap = None;
140 for line in contents.lines() {
141 if line.starts_with("MemTotal:") {
142 memory = Some(parse_kib_line(line)?);
143 } else if line.starts_with("SwapTotal:") {
144 swap = Some(parse_kib_line(line)?);
145 }
146 }
147
148 let mem_total = memory.ok_or_else(|| anyhow!("MemTotal not found"))?;
149 let swap_total = swap.ok_or_else(|| anyhow!("SwapTotal not found"))?;
150
151 Ok(Self {
152 mem_total,
153 swap_total,
154 })
155 }
156 }
157
158 fn get_physical_limits() -> Option<(u64, u64)> {
160 let meminfo = match ProcMemInfo::from_proc() {
161 Ok(meminfo) => meminfo,
162 Err(error) => {
163 error!("reading `/proc/meminfo`: {error}");
164 return None;
165 }
166 };
167
168 Some((meminfo.mem_total, meminfo.swap_total))
169 }
170
171 fn get_cgroup_limits() -> (Option<u64>, Option<u64>) {
179 let Ok(proc_cgroup) = fs::read_to_string("/proc/self/cgroup") else {
180 return (None, None);
181 };
182
183 let mut lines = proc_cgroup.lines();
187 let Some(cgroup_path) = lines.find_map(|l| l.strip_prefix("0::")) else {
188 error!("invalid `/proc/self/cgroup` format: {proc_cgroup}");
189 return (None, None);
190 };
191
192 let cgroup_path = cgroup_path.strip_prefix("/").unwrap_or(cgroup_path);
195
196 let root = Path::new("/sys/fs/cgroup").join(cgroup_path);
197 if !root.exists() {
198 error!("invalid cgroup root: {}", root.display());
199 return (None, None);
200 }
201
202 let memory_file = root.join("memory.max");
203 let swap_file = root.join("memory.swap.max");
204
205 let memory = fs::read_to_string(memory_file)
206 .ok()
207 .and_then(|s| s.trim().parse().ok());
208 let swap = fs::read_to_string(swap_file)
209 .ok()
210 .and_then(|s| s.trim().parse().ok());
211
212 (memory, swap)
213 }
214}
215
216#[cfg(not(target_os = "linux"))]
217mod macos {
218 use mz_compute::memory_limiter;
219 use mz_ore::cast::CastInto;
220
221 pub fn collect_heap_usage() -> (Option<u64>, Option<u64>) {
222 (None, None)
223 }
224
225 pub fn collect_heap_limit() -> Option<u64> {
226 memory_limiter::get_memory_limit().map(CastInto::cast_into)
227 }
228}
229
230#[cfg(target_os = "linux")]
231use linux::*;
232#[cfg(not(target_os = "linux"))]
233use macos::*;