Skip to main content

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` (`$icon`)
72//! - `memory_swap` (`$icon_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(crate) fn prepare(config: &Config) -> Result<Arc<BlockPlan>> {
100    // Both icons are available in every format, and every numeric value
101    // below is inserted on every render.
102    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        // TODO: possibly remove this as it is confusing to have `mem_total_used` and `mem_used`
131        // htop and such only display equivalent of `mem_used`
132        let mem_total_used = mem_total - mem_free;
133
134        // dev note: difference between avail and free:
135        // https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=34e431b0ae398fc54ea69ff85ec700722c9da773
136        // same logic as htop
137        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        // While zfs_arc_cache can be considered "available" memory,
144        // it can only free a maximum of (zfs_arc_cache - zfs_arc_min) amount.
145        // see https://github.com/htop-dev/htop/pull/1003
146        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        // See https://lore.kernel.org/lkml/1455827801-13082-1-git-send-email-hannes@cmpxchg.org/
156        let cached = pagecache + reclaimable - shmem + zfs_shrinkable_size;
157
158        let buffers = mem_state.buffers as f64;
159
160        // Userspace should use `mem_avail` for estimating the memory that is available.
161        // See: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=34e431b0ae398fc54ea69ff85ec700722c9da773
162        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        // Zswap usage
170        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        // Zram usage
185        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        // Reference: https://www.kernel.org/doc/Documentation/filesystems/proc.txt
291        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            // These values are reported as “kB” but are actually “kiB”.
321            // Convert them into bytes to avoid having to handle this later.
322            // Source: https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/6/html/deployment_guide/s2-proc-meminfo#s2-proc-meminfo
323            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        // For ZRAM
344        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 initializes with small amount by default, return 0 then
373                && 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        // For ZFS
381        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}