1use std::cmp::min;
74use std::str::FromStr as _;
75use tokio::fs::{File, read_dir};
76use tokio::io::{AsyncBufReadExt as _, BufReader};
77
78use super::prelude::*;
79use crate::util::read_file;
80
81#[derive(Deserialize, Debug, SmartDefault)]
82#[serde(deny_unknown_fields, default)]
83pub struct Config {
84 pub format: FormatConfig,
85 pub format_alt: Option<FormatConfig>,
86 #[default(5.into())]
87 pub interval: Seconds,
88 #[default(80.0)]
89 pub warning_mem: f64,
90 #[default(80.0)]
91 pub warning_swap: f64,
92 #[default(95.0)]
93 pub critical_mem: f64,
94 #[default(95.0)]
95 pub critical_swap: f64,
96}
97
98pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
99 let mut actions = api.get_actions()?;
100 api.set_default_actions(&[(MouseButton::Left, None, "toggle_format")])?;
101
102 let mut format = config.format.with_default(
103 " $icon $mem_used.eng(prefix:Mi)/$mem_total.eng(prefix:Mi)($mem_used_percents.eng(w:2)) ",
104 )?;
105 let mut format_alt = match &config.format_alt {
106 Some(f) => Some(f.with_default("")?),
107 None => None,
108 };
109
110 let mut timer = config.interval.timer();
111
112 loop {
113 let mem_state = Memstate::new().await?;
114
115 let mem_total = mem_state.mem_total as f64 * 1024.;
116 let mem_free = mem_state.mem_free as f64 * 1024.;
117
118 let mem_total_used = mem_total - mem_free;
121
122 let mem_avail = if mem_state.mem_available != 0 {
126 min(mem_state.mem_available, mem_state.mem_total)
127 } else {
128 mem_state.mem_free
129 } as f64
130 * 1024.;
131
132 let zfs_shrinkable_size = mem_state
136 .zfs_arc_cache
137 .saturating_sub(mem_state.zfs_arc_min) as f64;
138 let mem_avail = mem_avail + zfs_shrinkable_size;
139
140 let pagecache = mem_state.pagecache as f64 * 1024.;
141 let reclaimable = mem_state.s_reclaimable as f64 * 1024.;
142 let shmem = mem_state.shmem as f64 * 1024.;
143
144 let cached = pagecache + reclaimable - shmem + zfs_shrinkable_size;
146
147 let buffers = mem_state.buffers as f64 * 1024.;
148
149 let used_diff = mem_free + buffers + pagecache + reclaimable;
151 let mem_used = if mem_total >= used_diff {
152 mem_total - used_diff
153 } else {
154 mem_total - mem_free
155 };
156
157 let mem_used = mem_used - zfs_shrinkable_size;
159
160 let swap_total = mem_state.swap_total as f64 * 1024.;
161 let swap_free = mem_state.swap_free as f64 * 1024.;
162 let swap_cached = mem_state.swap_cached as f64 * 1024.;
163 let swap_used = swap_total - swap_free - swap_cached;
164
165 let zswap_compressed = mem_state.zswap_compressed as f64 * 1024.;
167 let zswap_decompressed = mem_state.zswap_decompressed as f64 * 1024.;
168
169 let zswap_comp_ratio = if zswap_compressed != 0.0 {
170 zswap_decompressed / zswap_compressed
171 } else {
172 0.0
173 };
174 let zswap_decompressed_percents = if (swap_used + swap_cached) != 0.0 {
175 zswap_decompressed / (swap_used + swap_cached) * 100.0
176 } else {
177 0.0
178 };
179
180 let zram_compressed = mem_state.zram_compressed as f64;
182 let zram_decompressed = mem_state.zram_decompressed as f64;
183
184 let zram_comp_ratio = if zram_compressed != 0.0 {
185 zram_decompressed / zram_compressed
186 } else {
187 0.0
188 };
189
190 let mut widget = Widget::new().with_format(format.clone());
191 widget.set_values(map! {
192 "icon" => Value::icon("memory_mem"),
193 "icon_swap" => Value::icon("memory_swap"),
194 "mem_total" => Value::bytes(mem_total),
195 "mem_free" => Value::bytes(mem_free),
196 "mem_free_percents" => Value::percents(mem_free / mem_total * 100.),
197 "mem_total_used" => Value::bytes(mem_total_used),
198 "mem_total_used_percents" => Value::percents(mem_total_used / mem_total * 100.),
199 "mem_used" => Value::bytes(mem_used),
200 "mem_used_percents" => Value::percents(mem_used / mem_total * 100.),
201 "mem_avail" => Value::bytes(mem_avail),
202 "mem_avail_percents" => Value::percents(mem_avail / mem_total * 100.),
203 "swap_total" => Value::bytes(swap_total),
204 "swap_free" => Value::bytes(swap_free),
205 "swap_free_percents" => Value::percents(swap_free / swap_total * 100.),
206 "swap_used" => Value::bytes(swap_used),
207 "swap_used_percents" => Value::percents(swap_used / swap_total * 100.),
208 "buffers" => Value::bytes(buffers),
209 "buffers_percent" => Value::percents(buffers / mem_total * 100.),
210 "cached" => Value::bytes(cached),
211 "cached_percent" => Value::percents(cached / mem_total * 100.),
212 "zram_compressed" => Value::bytes(zram_compressed),
213 "zram_decompressed" => Value::bytes(zram_decompressed),
214 "zram_comp_ratio" => Value::number(zram_comp_ratio),
215 "zswap_compressed" => Value::bytes(zswap_compressed),
216 "zswap_decompressed" => Value::bytes(zswap_decompressed),
217 "zswap_decompressed_percents" => Value::percents(zswap_decompressed_percents),
218 "zswap_comp_ratio" => Value::number(zswap_comp_ratio),
219 });
220
221 let mem_state = match mem_used / mem_total * 100. {
222 x if x > config.critical_mem => State::Critical,
223 x if x > config.warning_mem => State::Warning,
224 _ => State::Idle,
225 };
226
227 let swap_state = match swap_used / swap_total * 100. {
228 x if x > config.critical_swap => State::Critical,
229 x if x > config.warning_swap => State::Warning,
230 _ => State::Idle,
231 };
232
233 widget.state = if mem_state == State::Critical || swap_state == State::Critical {
234 State::Critical
235 } else if mem_state == State::Warning || swap_state == State::Warning {
236 State::Warning
237 } else {
238 State::Idle
239 };
240
241 api.set_widget(widget)?;
242
243 loop {
244 select! {
245 _ = timer.tick() => break,
246 _ = api.wait_for_update_request() => break,
247 Some(action) = actions.recv() => match action.as_ref() {
248 "toggle_format" => {
249 if let Some(ref mut format_alt) = format_alt {
250 std::mem::swap(format_alt, &mut format);
251 break;
252 }
253 }
254 _ => (),
255 }
256 }
257 }
258 }
259}
260
261#[derive(Clone, Copy, Debug, Default)]
262struct Memstate {
263 mem_total: u64,
264 mem_free: u64,
265 mem_available: u64,
266 buffers: u64,
267 pagecache: u64,
268 s_reclaimable: u64,
269 shmem: u64,
270 swap_total: u64,
271 swap_free: u64,
272 swap_cached: u64,
273 zram_compressed: u64,
274 zram_decompressed: u64,
275 zswap_compressed: u64,
276 zswap_decompressed: u64,
277 zfs_arc_cache: u64,
278 zfs_arc_min: u64,
279}
280
281impl Memstate {
282 async fn new() -> Result<Self> {
283 let mut file = BufReader::new(
285 File::open("/proc/meminfo")
286 .await
287 .error("/proc/meminfo does not exist")?,
288 );
289
290 let mut mem_state = Memstate::default();
291 let mut line = String::new();
292
293 while file
294 .read_line(&mut line)
295 .await
296 .error("failed to read /proc/meminfo")?
297 != 0
298 {
299 let mut words = line.split_whitespace();
300
301 let name = match words.next() {
302 Some(name) => name,
303 None => {
304 line.clear();
305 continue;
306 }
307 };
308 let val = words
309 .next()
310 .and_then(|x| u64::from_str(x).ok())
311 .error("failed to parse /proc/meminfo")?;
312
313 match name {
314 "MemTotal:" => mem_state.mem_total = val,
315 "MemFree:" => mem_state.mem_free = val,
316 "MemAvailable:" => mem_state.mem_available = val,
317 "Buffers:" => mem_state.buffers = val,
318 "Cached:" => mem_state.pagecache = val,
319 "SReclaimable:" => mem_state.s_reclaimable = val,
320 "Shmem:" => mem_state.shmem = val,
321 "SwapTotal:" => mem_state.swap_total = val,
322 "SwapFree:" => mem_state.swap_free = val,
323 "SwapCached:" => mem_state.swap_cached = val,
324 "Zswap:" => mem_state.zswap_compressed = val,
325 "Zswapped:" => mem_state.zswap_decompressed = val,
326 _ => (),
327 }
328
329 line.clear();
330 }
331
332 let mut entries = read_dir("/sys/block/")
334 .await
335 .error("Could not read /sys/block")?;
336 while let Some(entry) = entries
337 .next_entry()
338 .await
339 .error("Could not get next file /sys/block")?
340 {
341 let Ok(file_name) = entry.file_name().into_string() else {
342 continue;
343 };
344 if !file_name.starts_with("zram") {
345 continue;
346 }
347
348 let zram_file_path = entry.path().join("mm_stat");
349 let Ok(file) = File::open(zram_file_path).await else {
350 continue;
351 };
352
353 let mut buf = BufReader::new(file);
354 let mut line = String::new();
355 if buf.read_to_string(&mut line).await.is_err() {
356 continue;
357 }
358
359 let mut values = line.split_whitespace().map(|s| s.parse::<u64>());
360 let (Some(Ok(zram_swap_size)), Some(Ok(zram_comp_size))) =
361 (values.next(), values.next())
362 else {
363 continue;
364 };
365
366 if zram_swap_size >= 65_536 {
368 mem_state.zram_decompressed += zram_swap_size;
369 mem_state.zram_compressed += zram_comp_size;
370 }
371 }
372
373 if let Ok(arcstats) = read_file("/proc/spl/kstat/zfs/arcstats").await {
375 let size_re = regex!(r"size\s+\d+\s+(\d+)");
376 let size = &size_re
377 .captures(&arcstats)
378 .error("failed to find zfs_arc_cache size")?[1];
379 mem_state.zfs_arc_cache = size.parse().error("failed to parse zfs_arc_cache size")?;
380 let c_min_re = regex!(r"c_min\s+\d+\s+(\d+)");
381 let c_min = &c_min_re
382 .captures(&arcstats)
383 .error("failed to find zfs_arc_min size")?[1];
384 mem_state.zfs_arc_min = c_min.parse().error("failed to parse zfs_arc_min size")?;
385 }
386
387 Ok(mem_state)
388 }
389}