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