Skip to main content

i3status_rs/blocks/
cpu.rs

1//! CPU statistics
2//!
3//! # Configuration
4//!
5//! Key | Values | Default
6//! ----|--------|--------
7//! `format` | A [MultiFormat][MaybeMultiFormatConfig] string to customise the output of this block. See below for available placeholders. | `[" $icon $utilization "]`
8//! `interval` | Update interval in seconds | `5`
9//! `info_cpu` | Percentage of CPU usage, where state is set to info | `30.0`
10//! `warning_cpu` | Percentage of CPU usage, where state is set to warning | `60.0`
11//! `critical_cpu` | Percentage of CPU usage, where state is set to critical | `90.0`
12//!
13//! Placeholder      | Value                                                                | Type   | Unit
14//! -----------------|----------------------------------------------------------------------|--------|---------------
15//! `icon`           | An icon                                                              | Icon   | -
16//! `utilization`    | Average CPU utilization                                              | Number | %
17//! `utilization<N>` | Utilization of Nth logical CPU                                       | Number | %
18//! `barchart`       | Utilization of all logical CPUs presented as a barchart              | Text   | -
19//! `frequency`      | Average CPU frequency (may be absent if CPU is not supported)        | Number | Hz
20//! `frequency<N>`   | Frequency of Nth logical CPU (may be absent if CPU is not supported) | Number | Hz
21//! `max_frequency`  | Max frequency of all logical CPUs                                    | Number | Hz
22//! `boost`          | CPU turbo boost status (may be absent if CPU is not supported)       | Text   | -
23//!
24//! Action          | Description                               | Default button
25//! ----------------|-------------------------------------------|---------------
26//! `toggle_format` **DEPRECATED** | Toggles between `format` and `format_alt` | -
27//! `next_format`  | Switches to the next format in the list     | Left
28//! `prev_format`  | Switches to the previous format in the list | Right
29//!
30//! # Example
31//!
32//! ```toml
33//! [[block]]
34//! block = "cpu"
35//! interval = 1
36//! format = " $icon $barchart $utilization "
37//! format_alt = " $icon $frequency{ $boost|} "
38//! info_cpu = 20
39//! warning_cpu = 50
40//! critical_cpu = 90
41//! ```
42//!
43//! # Icons Used
44//! - `cpu` (as a progression)
45//! - `cpu_boost_on`
46//! - `cpu_boost_off`
47
48use std::str::FromStr as _;
49
50use tokio::fs::File;
51use tokio::io::{AsyncBufReadExt as _, BufReader};
52
53use super::prelude::*;
54use crate::util::read_file;
55
56const CPU_BOOST_PATH: &str = "/sys/devices/system/cpu/cpufreq/boost";
57const CPU_NO_TURBO_PATH: &str = "/sys/devices/system/cpu/intel_pstate/no_turbo";
58
59#[derive(Deserialize, Debug, SmartDefault)]
60#[serde(default)]
61pub struct Config {
62    #[serde(flatten)]
63    pub formats: MaybeMultiFormatConfig,
64    #[default(5.into())]
65    pub interval: Seconds,
66    #[default(30.0)]
67    pub info_cpu: f64,
68    #[default(60.0)]
69    pub warning_cpu: f64,
70    #[default(90.0)]
71    pub critical_cpu: f64,
72}
73
74pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
75    let mut actions = api.get_actions()?;
76    api.set_default_actions(&[
77        (MouseButton::Left, None, "next_format"),
78        (MouseButton::Right, None, "prev_format"),
79    ])?;
80
81    let mut formats = config.formats.with_default(" $icon $utilization ")?;
82
83    // Store previous /proc/stat state
84    let mut cputime = read_proc_stat().await?;
85    let cores = cputime.1.len();
86
87    if cores == 0 {
88        return Err(Error::new("/proc/stat reported zero cores"));
89    }
90
91    let mut timer = config.interval.timer();
92
93    loop {
94        let freqs = read_frequencies().await?;
95
96        // Compute utilizations
97        let new_cputime = read_proc_stat().await?;
98        let utilization_avg = new_cputime.0.utilization(cputime.0);
99        let mut utilizations = Vec::new();
100        if new_cputime.1.len() != cores {
101            return Err(Error::new("new cputime length is incorrect"));
102        }
103        for i in 0..cores {
104            utilizations.push(new_cputime.1[i].utilization(cputime.1[i]));
105        }
106        cputime = new_cputime;
107
108        // Create barchart indicating per-core utilization
109        let mut barchart = String::new();
110        const BOXCHARS: &[char] = &['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
111        for utilization in &utilizations {
112            barchart.push(BOXCHARS[(7.5 * utilization) as usize]);
113        }
114
115        // Read boost state on intel CPUs
116        let boost = boost_status().await.map(|status| match status {
117            true => "cpu_boost_on",
118            false => "cpu_boost_off",
119        });
120
121        let mut values = map!(
122            "icon" => Value::icon_progression("cpu", utilization_avg),
123            "barchart" => Value::text(barchart),
124            "utilization" => Value::percents(utilization_avg * 100.),
125            [if !freqs.is_empty()] "frequency" => Value::hertz(freqs.iter().sum::<f64>() / (freqs.len() as f64)),
126            [if !freqs.is_empty()] "max_frequency" => Value::hertz(freqs.iter().copied().max_by(f64::total_cmp).unwrap()),
127        );
128        boost.map(|b| values.insert("boost".into(), Value::icon(b)));
129        for (i, freq) in freqs.iter().enumerate() {
130            values.insert(format!("frequency{}", i + 1).into(), Value::hertz(*freq));
131        }
132        for (i, utilization) in utilizations.iter().enumerate() {
133            values.insert(
134                format!("utilization{}", i + 1).into(),
135                Value::percents(utilization * 100.),
136            );
137        }
138
139        let mut widget = Widget::new().with_format(formats.get_format());
140        widget.set_values(values);
141        widget.state = match utilization_avg * 100. {
142            x if x > config.critical_cpu => State::Critical,
143            x if x > config.warning_cpu => State::Warning,
144            x if x > config.info_cpu => State::Info,
145            _ => State::Idle,
146        };
147        api.set_widget(widget)?;
148
149        loop {
150            select! {
151                _ = timer.tick() => break,
152                _ = api.wait_for_update_request() => break,
153                Some(action) = actions.recv() => match action.as_ref() {
154                    "next_format" | "toggle_format" => {
155                        formats.next_format();
156                        break;
157                    }
158                    "prev_format" => {
159                        formats.prev_format();
160                        break;
161                    }
162                    _ => (),
163                }
164            }
165        }
166    }
167}
168
169// Read frequencies (read in MHz, store in Hz)
170async fn read_frequencies() -> Result<Vec<f64>> {
171    let mut freqs = Vec::with_capacity(32);
172
173    let file = File::open("/proc/cpuinfo")
174        .await
175        .error("failed to read /proc/cpuinfo")?;
176    let mut file = BufReader::new(file);
177
178    let mut line = String::new();
179    while file
180        .read_line(&mut line)
181        .await
182        .error("failed to read /proc/cpuinfo")?
183        != 0
184    {
185        if line.starts_with("cpu MHz") {
186            let slice = line
187                .trim_end()
188                .trim_start_matches(|c: char| !c.is_ascii_digit());
189            freqs.push(f64::from_str(slice).error("failed to parse /proc/cpuinfo")? * 1e6);
190        }
191        line.clear();
192    }
193
194    Ok(freqs)
195}
196
197#[derive(Debug, Clone, Copy)]
198struct CpuTime {
199    idle: u64,
200    non_idle: u64,
201}
202
203impl CpuTime {
204    fn from_str(s: &str) -> Option<Self> {
205        let mut s = s.trim().split_ascii_whitespace();
206        let user = u64::from_str(s.next()?).ok()?;
207        let nice = u64::from_str(s.next()?).ok()?;
208        let system = u64::from_str(s.next()?).ok()?;
209        let idle = u64::from_str(s.next()?).ok()?;
210        let iowait = u64::from_str(s.next()?).ok()?;
211        let irq = u64::from_str(s.next()?).ok()?;
212        let softirq = u64::from_str(s.next()?).ok()?;
213
214        Some(Self {
215            idle: idle + iowait,
216            non_idle: user + nice + system + irq + softirq,
217        })
218    }
219
220    fn utilization(&self, old: Self) -> f64 {
221        let elapsed = (self.idle + self.non_idle).saturating_sub(old.idle + old.non_idle);
222        if elapsed == 0 {
223            0.0
224        } else {
225            ((self.non_idle - old.non_idle) as f64 / elapsed as f64).clamp(0., 1.)
226        }
227    }
228}
229
230async fn read_proc_stat() -> Result<(CpuTime, Vec<CpuTime>)> {
231    let mut utilizations = Vec::with_capacity(32);
232    let mut total = None;
233
234    let file = File::open("/proc/stat")
235        .await
236        .error("failed to read /proc/stat")?;
237    let mut file = BufReader::new(file);
238
239    let mut line = String::new();
240    while file
241        .read_line(&mut line)
242        .await
243        .error("failed to read /proc/stat")?
244        != 0
245    {
246        // Total time
247        let data = line.trim_start_matches(|c: char| !c.is_ascii_whitespace());
248        if line.starts_with("cpu ") {
249            total = Some(CpuTime::from_str(data).error("failed to parse /proc/stat")?);
250        } else if line.starts_with("cpu") {
251            utilizations.push(CpuTime::from_str(data).error("failed to parse /proc/stat")?);
252        }
253        line.clear();
254    }
255
256    Ok((total.error("failed to parse /proc/stat")?, utilizations))
257}
258
259/// Read the cpu turbo boost status from kernel sys interface
260/// or intel pstate interface
261async fn boost_status() -> Option<bool> {
262    if let Ok(boost) = read_file(CPU_BOOST_PATH).await {
263        Some(boost.starts_with('1'))
264    } else if let Ok(no_turbo) = read_file(CPU_NO_TURBO_PATH).await {
265        Some(no_turbo.starts_with('0'))
266    } else {
267        None
268    }
269}