i3status_rs/blocks/memory.rs
1//! Memory and swap usage
2//!
3//! # Configuration
4//!
5//! Key | Values | Default
6//! ----|--------|--------
7//! `format` | A [MultiFormat][MaybeMultiFormatConfig] string to customise the output of this block when in "Memory" view. See below for available placeholders. | `[" $icon $mem_used.eng(prefix:Mi)/$mem_total.eng(prefix:Mi)($mem_used_percents.eng(w:2)) "]`
8//! `interval` | Update interval in seconds | `5`
9//! `warning_mem` | Percentage of memory usage, where state is set to warning | `80.0`
10//! `warning_swap` | Percentage of swap usage, where state is set to warning | `80.0`
11//! `critical_mem` | Percentage of memory usage, where state is set to critical | `95.0`
12//! `critical_swap` | Percentage of swap usage, where state is set to critical | `95.0`
13//!
14//! Placeholder | Value | Type | Unit
15//! --------------------------|---------------------------------------------------------------------------------|--------|-------
16//! `icon` | Memory icon | Icon | -
17//! `icon_swap` | Swap icon | Icon | -
18//! `mem_total` | Total physical ram available | Number | Bytes
19//! `mem_free` | Free memory not yet used by the kernel or userspace (in general you should use mem_avail) | Number | Bytes
20//! `mem_free_percents` | as above but as a percentage of total memory | Number | Percents
21//! `mem_avail` | Kernel estimate of usable free memory which includes cached memory and buffers | Number | Bytes
22//! `mem_avail_percents` | as above but as a percentage of total memory | Number | Percents
23//! `mem_total_used` | mem_total - mem_free | Number | Bytes
24//! `mem_total_used_percents` | as above but as a percentage of total memory | Number | Percents
25//! `mem_used` | Memory used, excluding cached memory and buffers; same as htop's green bar | Number | Bytes
26//! `mem_used_percents` | as above but as a percentage of total memory | Number | Percents
27//! `buffers` | Buffers, similar to htop's blue bar | Number | Bytes
28//! `buffers_percent` | as above but as a percentage of total memory | Number | Percents
29//! `cached` | Cached memory (taking into account ZFS ARC cache), similar to htop's yellow bar | Number | Bytes
30//! `cached_percent` | as above but as a percentage of total memory | Number | Percents
31//! `swap_total` | Swap total | Number | Bytes
32//! `swap_free` | Swap free | Number | Bytes
33//! `swap_free_percents` | as above but as a percentage of total memory | Number | Percents
34//! `swap_used` | Swap used | Number | Bytes
35//! `swap_used_percents` | as above but as a percentage of total memory | Number | Percents
36//! `zram_compressed` | Compressed zram memory usage | Number | Bytes
37//! `zram_decompressed` | Decompressed zram memory usage | Number | Bytes
38//! 'zram_comp_ratio' | Ratio of the decompressed/compressed zram memory | Number | -
39//! `zswap_compressed` | Compressed zswap memory usage (>=Linux 5.19) | Number | Bytes
40//! `zswap_decompressed` | Decompressed zswap memory usage (>=Linux 5.19) | Number | Bytes
41//! `zswap_decompressed_percents` | as above but as a percentage of total zswap memory (>=Linux 5.19) | Number | Percents
42//! 'zswap_comp_ratio' | Ratio of the decompressed/compressed zswap memory (>=Linux 5.19) | Number | -
43//!
44//! Action | Description | Default button
45//! ----------------|-------------------------------------------|---------------
46//! `toggle_format` **DEPRECATED** | Toggles between `format` and `format_alt` | -
47//! `next_format` | Switches to the next format in the list | Left
48//! `prev_format` | Switches to the previous format in the list | Right
49//!
50//! # Examples
51//!
52//! ```toml
53//! [[block]]
54//! block = "memory"
55//! format = " $icon $mem_used_percents.eng(w:1) "
56//! format_alt = " $icon_swap $swap_free.eng(w:3,u:B,p:Mi)/$swap_total.eng(w:3,u:B,p:Mi)($swap_used_percents.eng(w:2)) "
57//! interval = 30
58//! warning_mem = 70
59//! critical_mem = 90
60//! ```
61//!
62//! Show swap and hide if it is zero:
63//!
64//! ```toml
65//! [[block]]
66//! block = "memory"
67//! format = " $icon $swap_used.eng(range:1..) |"
68//! ```
69//!
70//! # Icons Used
71//! - `memory_mem`
72//! - `memory_swap`
73
74use 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 async fn run(config: &Config, api: &CommonApi) -> Result<()> {
100 let mut actions = api.get_actions()?;
101 api.set_default_actions(&[
102 (MouseButton::Left, None, "next_format"),
103 (MouseButton::Right, None, "prev_format"),
104 ])?;
105
106 let mut formats = config.formats.with_default(
107 " $icon $mem_used.eng(prefix:Mi)/$mem_total.eng(prefix:Mi)($mem_used_percents.eng(w:2)) ",
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;
116 let mem_free = mem_state.mem_free as f64;
117
118 // TODO: possibly remove this as it is confusing to have `mem_total_used` and `mem_used`
119 // htop and such only display equivalent of `mem_used`
120 let mem_total_used = mem_total - mem_free;
121
122 // dev note: difference between avail and free:
123 // https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=34e431b0ae398fc54ea69ff85ec700722c9da773
124 // same logic as htop
125 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
131 // While zfs_arc_cache can be considered "available" memory,
132 // it can only free a maximum of (zfs_arc_cache - zfs_arc_min) amount.
133 // see https://github.com/htop-dev/htop/pull/1003
134 let zfs_shrinkable_size = mem_state
135 .zfs_arc_cache
136 .saturating_sub(mem_state.zfs_arc_min) as f64;
137 let mem_avail = mem_avail + zfs_shrinkable_size;
138
139 let pagecache = mem_state.pagecache as f64;
140 let reclaimable = mem_state.s_reclaimable as f64;
141 let shmem = mem_state.shmem as f64;
142
143 // See https://lore.kernel.org/lkml/1455827801-13082-1-git-send-email-hannes@cmpxchg.org/
144 let cached = pagecache + reclaimable - shmem + zfs_shrinkable_size;
145
146 let buffers = mem_state.buffers as f64;
147
148 // Userspace should use `mem_avail` for estimating the memory that is available.
149 // See: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=34e431b0ae398fc54ea69ff85ec700722c9da773
150 let mem_used = mem_total - mem_avail;
151
152 let swap_total = mem_state.swap_total as f64;
153 let swap_free = mem_state.swap_free as f64;
154 let swap_cached = mem_state.swap_cached as f64;
155 let swap_used = swap_total - swap_free - swap_cached;
156
157 // Zswap usage
158 let zswap_compressed = mem_state.zswap_compressed as f64;
159 let zswap_decompressed = mem_state.zswap_decompressed as f64;
160
161 let zswap_comp_ratio = if zswap_compressed != 0.0 {
162 zswap_decompressed / zswap_compressed
163 } else {
164 0.0
165 };
166 let zswap_decompressed_percents = if (swap_used + swap_cached) != 0.0 {
167 zswap_decompressed / (swap_used + swap_cached) * 100.0
168 } else {
169 0.0
170 };
171
172 // Zram usage
173 let zram_compressed = mem_state.zram_compressed as f64;
174 let zram_decompressed = mem_state.zram_decompressed as f64;
175
176 let zram_comp_ratio = if zram_compressed != 0.0 {
177 zram_decompressed / zram_compressed
178 } else {
179 0.0
180 };
181
182 let mut widget = Widget::new().with_format(formats.get_format());
183 widget.set_values(map! {
184 "icon" => Value::icon("memory_mem"),
185 "icon_swap" => Value::icon("memory_swap"),
186 "mem_total" => Value::bytes(mem_total),
187 "mem_free" => Value::bytes(mem_free),
188 "mem_free_percents" => Value::percents(mem_free / mem_total * 100.),
189 "mem_total_used" => Value::bytes(mem_total_used),
190 "mem_total_used_percents" => Value::percents(mem_total_used / mem_total * 100.),
191 "mem_used" => Value::bytes(mem_used),
192 "mem_used_percents" => Value::percents(mem_used / mem_total * 100.),
193 "mem_avail" => Value::bytes(mem_avail),
194 "mem_avail_percents" => Value::percents(mem_avail / mem_total * 100.),
195 "swap_total" => Value::bytes(swap_total),
196 "swap_free" => Value::bytes(swap_free),
197 "swap_free_percents" => Value::percents(swap_free / swap_total * 100.),
198 "swap_used" => Value::bytes(swap_used),
199 "swap_used_percents" => Value::percents(swap_used / swap_total * 100.),
200 "buffers" => Value::bytes(buffers),
201 "buffers_percent" => Value::percents(buffers / mem_total * 100.),
202 "cached" => Value::bytes(cached),
203 "cached_percent" => Value::percents(cached / mem_total * 100.),
204 "zram_compressed" => Value::bytes(zram_compressed),
205 "zram_decompressed" => Value::bytes(zram_decompressed),
206 "zram_comp_ratio" => Value::number(zram_comp_ratio),
207 "zswap_compressed" => Value::bytes(zswap_compressed),
208 "zswap_decompressed" => Value::bytes(zswap_decompressed),
209 "zswap_decompressed_percents" => Value::percents(zswap_decompressed_percents),
210 "zswap_comp_ratio" => Value::number(zswap_comp_ratio),
211 });
212
213 let mem_state = match mem_used / mem_total * 100. {
214 x if x > config.critical_mem => State::Critical,
215 x if x > config.warning_mem => State::Warning,
216 _ => State::Idle,
217 };
218
219 let swap_state = match swap_used / swap_total * 100. {
220 x if x > config.critical_swap => State::Critical,
221 x if x > config.warning_swap => State::Warning,
222 _ => State::Idle,
223 };
224
225 widget.state = if mem_state == State::Critical || swap_state == State::Critical {
226 State::Critical
227 } else if mem_state == State::Warning || swap_state == State::Warning {
228 State::Warning
229 } else {
230 State::Idle
231 };
232
233 api.set_widget(widget)?;
234
235 loop {
236 select! {
237 _ = timer.tick() => break,
238 _ = api.wait_for_update_request() => break,
239 Some(action) = actions.recv() => match action.as_ref() {
240 "next_format" | "toggle_format" => {
241 formats.next_format();
242 break;
243 }
244 "prev_format" => {
245 formats.prev_format();
246 break;
247 }
248 _ => (),
249 }
250 }
251 }
252 }
253}
254
255#[derive(Clone, Copy, Debug, Default)]
256struct Memstate {
257 mem_total: u64,
258 mem_free: u64,
259 mem_available: u64,
260 buffers: u64,
261 pagecache: u64,
262 s_reclaimable: u64,
263 shmem: u64,
264 swap_total: u64,
265 swap_free: u64,
266 swap_cached: u64,
267 zram_compressed: u64,
268 zram_decompressed: u64,
269 zswap_compressed: u64,
270 zswap_decompressed: u64,
271 zfs_arc_cache: u64,
272 zfs_arc_min: u64,
273}
274
275impl Memstate {
276 async fn new() -> Result<Self> {
277 // Reference: https://www.kernel.org/doc/Documentation/filesystems/proc.txt
278 let mut file = BufReader::new(
279 File::open("/proc/meminfo")
280 .await
281 .error("/proc/meminfo does not exist")?,
282 );
283
284 let mut mem_state = Memstate::default();
285 let mut line = String::new();
286
287 while file
288 .read_line(&mut line)
289 .await
290 .error("failed to read /proc/meminfo")?
291 != 0
292 {
293 let mut words = line.split_whitespace();
294
295 let name = match words.next() {
296 Some(name) => name,
297 None => {
298 line.clear();
299 continue;
300 }
301 };
302 let val = words
303 .next()
304 .and_then(|x| u64::from_str(x).ok())
305 .error("failed to parse /proc/meminfo")?;
306
307 // These values are reported as “kB” but are actually “kiB”.
308 // Convert them into bytes to avoid having to handle this later.
309 // Source: https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/6/html/deployment_guide/s2-proc-meminfo#s2-proc-meminfo
310 const KIB: u64 = 1024;
311 match name {
312 "MemTotal:" => mem_state.mem_total = val * KIB,
313 "MemFree:" => mem_state.mem_free = val * KIB,
314 "MemAvailable:" => mem_state.mem_available = val * KIB,
315 "Buffers:" => mem_state.buffers = val * KIB,
316 "Cached:" => mem_state.pagecache = val * KIB,
317 "SReclaimable:" => mem_state.s_reclaimable = val * KIB,
318 "Shmem:" => mem_state.shmem = val * KIB,
319 "SwapTotal:" => mem_state.swap_total = val * KIB,
320 "SwapFree:" => mem_state.swap_free = val * KIB,
321 "SwapCached:" => mem_state.swap_cached = val * KIB,
322 "Zswap:" => mem_state.zswap_compressed = val * KIB,
323 "Zswapped:" => mem_state.zswap_decompressed = val * KIB,
324 _ => (),
325 }
326
327 line.clear();
328 }
329
330 // For ZRAM
331 let mut entries = read_dir("/sys/block/")
332 .await
333 .error("Could not read /sys/block")?;
334 while let Some(entry) = entries
335 .next_entry()
336 .await
337 .error("Could not get next file /sys/block")?
338 {
339 let Ok(file_name) = entry.file_name().into_string() else {
340 continue;
341 };
342 if !file_name.starts_with("zram") {
343 continue;
344 }
345
346 let zram_file_path = entry.path().join("mm_stat");
347 let Ok(file) = File::open(zram_file_path).await else {
348 continue;
349 };
350
351 let mut buf = BufReader::new(file);
352 let mut line = String::new();
353 if buf.read_to_string(&mut line).await.is_err() {
354 continue;
355 }
356
357 let mut values = line.split_whitespace().map(|s| s.parse::<u64>());
358 if let Some(Ok(zram_swap_size)) = values.next() && let Some(Ok(zram_comp_size)) = values.next()
359 // zram initializes with small amount by default, return 0 then
360 && zram_swap_size >= 65_536
361 {
362 mem_state.zram_decompressed += zram_swap_size;
363 mem_state.zram_compressed += zram_comp_size;
364 }
365 }
366
367 // For ZFS
368 if let Ok(arcstats) = read_file("/proc/spl/kstat/zfs/arcstats").await {
369 let size_re = regex!(r"size\s+\d+\s+(\d+)");
370 let size = &size_re
371 .captures(&arcstats)
372 .error("failed to find zfs_arc_cache size")?[1];
373 mem_state.zfs_arc_cache = size.parse().error("failed to parse zfs_arc_cache size")?;
374 let c_min_re = regex!(r"c_min\s+\d+\s+(\d+)");
375 let c_min = &c_min_re
376 .captures(&arcstats)
377 .error("failed to find zfs_arc_min size")?[1];
378 mem_state.zfs_arc_min = c_min.parse().error("failed to parse zfs_arc_min size")?;
379 }
380
381 Ok(mem_state)
382 }
383}