i3status_rs/blocks/
memory.rs

1//! Memory and swap usage
2//!
3//! # Configuration
4//!
5//! Key | Values | Default
6//! ----|--------|--------
7//! `format` | A 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//! `format_alt` | If set, block will switch between `format` and `format_alt` on every click | `None`
9//! `interval` | Update interval in seconds | `5`
10//! `warning_mem` | Percentage of memory usage, where state is set to warning | `80.0`
11//! `warning_swap` | Percentage of swap usage, where state is set to warning | `80.0`
12//! `critical_mem` | Percentage of memory usage, where state is set to critical | `95.0`
13//! `critical_swap` | Percentage of swap usage, where state is set to critical | `95.0`
14//!
15//! Placeholder               | Value                                                                           | Type   | Unit
16//! --------------------------|---------------------------------------------------------------------------------|--------|-------
17//! `icon`                    | Memory icon                                                                     | Icon   | -
18//! `icon_swap`               | Swap icon                                                                       | Icon   | -
19//! `mem_total`               | Total physical ram available                                                    | Number | Bytes
20//! `mem_free`                | Free memory not yet used by the kernel or userspace (in general you should use mem_avail) | Number | Bytes
21//! `mem_free_percents`       | as above but as a percentage of total memory                                    | Number | Percents
22//! `mem_avail`               | Kernel estimate of usable free memory which includes cached memory and buffers  | Number | Bytes
23//! `mem_avail_percents`      | as above but as a percentage of total memory                                    | Number | Percents
24//! `mem_total_used`          | mem_total - mem_free                                                            | Number | Bytes
25//! `mem_total_used_percents` | as above but as a percentage of total memory                                    | Number | Percents
26//! `mem_used`                | Memory used, excluding cached memory and buffers; same as htop's green bar      | Number | Bytes
27//! `mem_used_percents`       | as above but as a percentage of total memory                                    | Number | Percents
28//! `buffers`                 | Buffers, similar to htop's blue bar                                             | Number | Bytes
29//! `buffers_percent`         | as above but as a percentage of total memory                                    | Number | Percents
30//! `cached`                  | Cached memory (taking into account ZFS ARC cache), similar to htop's yellow bar | Number | Bytes
31//! `cached_percent`          | as above but as a percentage of total memory                                    | Number | Percents
32//! `swap_total`              | Swap total                                                                      | Number | Bytes
33//! `swap_free`               | Swap free                                                                       | Number | Bytes
34//! `swap_free_percents`      | as above but as a percentage of total memory                                    | Number | Percents
35//! `swap_used`               | Swap used                                                                       | Number | Bytes
36//! `swap_used_percents`      | as above but as a percentage of total memory                                    | Number | Percents
37//! `zram_compressed`         | Compressed zram memory usage                                                    | Number | Bytes
38//! `zram_decompressed`       | Decompressed zram memory usage                                                  | Number | Bytes
39//! 'zram_comp_ratio'         | Ratio of the decompressed/compressed zram memory                                | Number | -
40//! `zswap_compressed`        | Compressed zswap memory usage (>=Linux 5.19)                                    | Number | Bytes
41//! `zswap_decompressed`      | Decompressed zswap memory usage (>=Linux 5.19)                                  | Number | Bytes
42//! `zswap_decompressed_percents` | as above but as a percentage of total zswap memory  (>=Linux 5.19)          | Number | Percents
43//! 'zswap_comp_ratio'        | Ratio of the decompressed/compressed zswap memory (>=Linux 5.19)                | Number | -
44//!
45//! Action          | Description                               | Default button
46//! ----------------|-------------------------------------------|---------------
47//! `toggle_format` | Toggles between `format` and `format_alt` | Left
48//!
49//! # Examples
50//!
51//! ```toml
52//! [[block]]
53//! block = "memory"
54//! format = " $icon $mem_used_percents.eng(w:1) "
55//! 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)) "
56//! interval = 30
57//! warning_mem = 70
58//! critical_mem = 90
59//! ```
60//!
61//! Show swap and hide if it is zero:
62//!
63//! ```toml
64//! [[block]]
65//! block = "memory"
66//! format = " $icon $swap_used.eng(range:1..) |"
67//! ```
68//!
69//! # Icons Used
70//! - `memory_mem`
71//! - `memory_swap`
72
73use 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        // 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            * 1024.;
131
132        // While zfs_arc_cache can be considered "available" memory,
133        // it can only free a maximum of (zfs_arc_cache - zfs_arc_min) amount.
134        // see https://github.com/htop-dev/htop/pull/1003
135        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        // See https://lore.kernel.org/lkml/1455827801-13082-1-git-send-email-hannes@cmpxchg.org/
145        let cached = pagecache + reclaimable - shmem + zfs_shrinkable_size;
146
147        let buffers = mem_state.buffers as f64 * 1024.;
148
149        // same logic as htop
150        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        // account for ZFS ARC cache
158        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        // Zswap usage
166        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        // Zram usage
181        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        // Reference: https://www.kernel.org/doc/Documentation/filesystems/proc.txt
284        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        // For ZRAM
333        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            // zram initializes with small amount by default, return 0 then
367            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        // For ZFS
374        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}