ferrix_lib/
cpu.rs

1/* cpu.rs
2 *
3 * Copyright 2025 Michail Krasnov <mskrasnov07@ya.ru>
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
17 *
18 * SPDX-License-Identifier: GPL-3.0-or-later
19 */
20
21//! Get information about installed CPUs
22//!
23//! Reads information from `/proc/cpuinfo` file
24//!
25//! ## Example
26//! ```
27//! use ferrix_lib::cpu::Processors;
28//! use ferrix_lib::traits::ToJson;
29//!
30//! let proc = Processors::new().unwrap();
31//! let json = proc.to_json().unwrap();
32//! dbg!(json);
33//! ```
34
35use anyhow::Result;
36use serde::{Deserialize, Serialize};
37use std::fs::read_to_string;
38
39use crate::traits::{ToJson, ToPlainText, print_opt_val};
40use crate::utils::Size;
41
42/// A structure containing data from the `/proc/cpuinfo` file
43#[derive(Debug, Serialize, Clone)]
44pub struct Processors {
45    /// Information about all core/thread
46    pub entries: Vec<CPU>,
47}
48
49impl Processors {
50    pub fn new() -> Result<Self> {
51        Ok(Self {
52            entries: read_info()?,
53        })
54    }
55}
56
57impl ToJson for Processors {}
58impl ToPlainText for Processors {
59    fn to_plain(&self) -> String {
60        let mut s = format!("Information about processors");
61        for proc in &self.entries {
62            s += &proc.to_plain();
63        }
64        s
65    }
66}
67
68/// A structure with data about each processor core/thread
69#[derive(Debug, Serialize, Default, Clone)]
70pub struct CPU {
71    /// Entry number (index)
72    pub processor: Option<usize>,
73
74    /************************ NOTE ***********************
75     *   Parameters for x86 and x86_64 architectures     *
76     *****************************************************/
77    /// Vendor name
78    pub vendor_id: Option<String>,
79
80    /// CPU Family ID
81    pub cpu_family: Option<u32>,
82
83    /// Model ID
84    pub model: Option<u32>,
85
86    /// Model name
87    pub model_name: Option<String>,
88
89    /// Stepping
90    pub stepping: Option<u32>,
91
92    /// Microcode number (representation as a `String`!)
93    pub microcode: Option<String>,
94
95    /// CPU core/thread *current* frequency
96    pub cpu_mhz: Option<f32>,
97
98    /// L3 cache size
99    pub cache_size: Option<Size>,
100
101    /// Physical ID of CPU core/thread
102    pub physical_id: Option<u32>,
103
104    /// Siblings
105    pub siblings: Option<u32>,
106
107    /// Core ID
108    pub core_id: Option<u32>,
109
110    /// CPU cores count
111    pub cpu_cores: Option<u32>,
112
113    /// APIC ID
114    pub apicid: Option<u32>,
115
116    /// Initial APIC ID
117    pub initial_apicid: Option<u32>,
118
119    /// Is FPU exists?
120    pub fpu: Option<bool>,
121
122    pub fpu_exception: Option<bool>,
123    pub cpuid_level: Option<u32>,
124    pub wp: Option<bool>,
125    pub flags: Option<Vec<String>>,
126    pub bugs: Option<Vec<String>>,
127    pub bogomips: Option<f64>,
128    pub clflush_size: Option<u32>,
129    pub cache_alignment: Option<u32>,
130    pub address_sizes: Option<String>,
131    pub power_management: Option<String>,
132
133    /************************ NOTE ***********************
134     *    Parameters for AArch64 (ARMv8) architecture    *
135     *****************************************************/
136    pub cpu_implementer: Option<String>,
137    pub cpu_architecture: Option<u8>,
138    pub cpu_variant: Option<String>,
139    pub cpu_part: Option<String>,
140    pub cpu_revision: Option<u32>,
141
142    /************************ NOTE ***********************
143     *   Parameters for ppc64le (PowerPC) architecture   *
144     *****************************************************/
145    pub cpu: Option<String>,
146    pub clock: Option<f32>,
147    pub revision: Option<String>,
148    pub timebase: Option<usize>,
149    pub platform: Option<String>,
150    pub machine: Option<String>,
151    pub model_ppc: Option<String>,
152}
153
154impl ToJson for CPU {}
155
156#[cfg(target_arch = "x86_64")]
157impl ToPlainText for CPU {
158    fn to_plain(&self) -> String {
159        let mut s = match self.processor {
160            Some(proc) => format!("\nProcessor #{proc}\n"),
161            None => format!("\nProcessor #unknown\n"),
162        };
163        s += "\tArchitecture: x86_64\n";
164        s += &print_opt_val("Vendor ID", &self.vendor_id);
165        s += &print_opt_val("CPU Family", &self.cpu_family);
166        s += &print_opt_val("CPU Model ID", &self.model);
167        s += &print_opt_val("CPU Model Name", &self.model_name);
168        s += &print_opt_val("Stepping", &self.stepping);
169        s += &print_opt_val("Microcode", &self.microcode);
170        s += &print_opt_val("Current frequency", &self.cpu_mhz);
171        s += &print_opt_val("L3 Cache Size", &self.cache_size);
172        s += &print_opt_val("Physical ID of CPU Core", &self.physical_id);
173        s += &print_opt_val("Siblings", &self.siblings);
174        s += &print_opt_val("Core ID", &self.core_id);
175        s += &print_opt_val("CPU cores", &self.cpu_cores);
176        s += &print_opt_val("APIC ID", &self.apicid);
177        s += &print_opt_val("Initial APIC ID", &self.initial_apicid);
178        s += &print_opt_val("FPU", &self.fpu);
179        s += &print_opt_val("FPU Exception", &self.fpu_exception);
180        s += &print_opt_val("CPUID Level", &self.cpuid_level);
181        s += &print_opt_val("WP", &self.wp);
182        s += &print_opt_val("Bogo MIPS", &self.bogomips);
183        s += &print_opt_val("Clflush Size", &self.clflush_size);
184        s += &print_opt_val("Cache alignment", &self.cache_alignment);
185        s += &print_opt_val("Address sizes", &self.address_sizes);
186        s += &print_opt_val("Power management", &self.power_management);
187
188        s
189    }
190}
191
192#[cfg(target_arch = "aarch64")]
193impl ToPlainText for CPU {
194    fn to_plain(&self) -> String {
195        let mut s = match self.processor {
196            Some(proc) => format!("Processor #{proc}\n"),
197            None => format!("Processor #unknown\n"),
198        };
199        s += &print_opt_val("CPU Implementer", &self.cpu_implementer);
200        s += &print_opt_val("CPU Architecture", &self.cpu_architecture);
201        s += &print_opt_val("CPU Variant", &self.cpu_variant);
202        s += &print_opt_val("CPU Part", &self.cpu_part);
203        s += &print_opt_val("CPU Revision", &self.cpu_revision);
204
205        s
206    }
207}
208
209fn read_info() -> Result<Vec<CPU>> {
210    let blocks = read_to_string("/proc/cpuinfo")?;
211    let blocks = blocks
212        .split("\n\n") // split by CPU blocks
213        .collect::<Vec<_>>();
214    let mut processors = Vec::with_capacity(blocks.len());
215
216    for block in blocks {
217        if block.trim().is_empty() {
218            continue;
219        }
220        let mut cpu = CPU::default();
221        for line in block.lines() {
222            parse_cpuinfo(&mut cpu, line);
223        }
224        processors.push(cpu);
225    }
226    Ok(processors)
227}
228
229fn get_parts(s: &str) -> impl Iterator<Item = &str> {
230    s.splitn(2, ':').map(|item| item.trim())
231}
232
233#[cfg(target_arch = "x86_64")]
234fn parse_cpuinfo(cpu: &mut CPU, parts: &str) {
235    let mut parts = get_parts(parts);
236    match (parts.next(), parts.next()) {
237        (Some(key), Some(val)) => match key {
238            "processor" => cpu.processor = val.parse().ok(),
239            "vendor_id" => cpu.vendor_id = Some(val.to_string()),
240            "cpu family" => cpu.cpu_family = val.parse().ok(),
241            "model" => cpu.model = val.parse().ok(),
242            "model name" => cpu.model_name = Some(val.to_string()),
243            "stepping" => cpu.stepping = val.parse().ok(),
244            "microcode" => cpu.microcode = Some(val.to_string()),
245            "cpu MHz" => cpu.cpu_mhz = val.parse().ok(),
246            "cache size" => cpu.cache_size = Size::try_from(val).ok(),
247            "physical id" => cpu.physical_id = val.parse().ok(),
248            "siblings" => cpu.siblings = val.parse().ok(),
249            "core id" => cpu.core_id = val.parse().ok(),
250            "cpu cores" => cpu.cpu_cores = val.parse().ok(),
251            "apicid" => cpu.apicid = val.parse().ok(),
252            "initial apicid" => cpu.initial_apicid = val.parse().ok(),
253            "fpu" => cpu.fpu = Some(get_bool(val)),
254            "fpu_exception" => cpu.fpu_exception = Some(get_bool(val)),
255            "cpuid level" => cpu.cpuid_level = val.parse().ok(),
256            "wp" => cpu.wp = Some(get_bool(val)),
257            "flags" | "Features" => {
258                cpu.flags = Some(val.split_whitespace().map(String::from).collect())
259            }
260            "bugs" => cpu.bugs = Some(val.split_whitespace().map(String::from).collect()),
261            "bogomips" | "BogoMIPS" => cpu.bogomips = val.parse().ok(),
262            "clflush size" => cpu.clflush_size = val.parse().ok(),
263            "cache_alignment" => cpu.cache_alignment = val.parse().ok(),
264            "address sizes" => cpu.address_sizes = Some(val.to_string()),
265            "power management" => cpu.power_management = Some(val.to_string()),
266            _ => {} // ignore unknown entry
267        },
268        _ => {}
269    }
270}
271
272#[cfg(target_arch = "aarch64")]
273fn parse_cpuinfo(cpu: &mut CPU, parts: &str) {
274    let mut parts = get_parts(parts);
275    match (parts.next(), parts.next()) {
276        (Some(key), Some(val)) => match key {
277            // ARM
278            "CPU implementer" => cpu.cpu_implementer = Some(val.to_string()),
279            "CPU architecture" => cpu.cpu_architecture = val.parse().ok(),
280            "CPU variant" => cpu.cpu_variant = Some(val.to_string()),
281            "CPU part" => cpu.cpu_part = Some(val.to_string()),
282            "CPU revision" => cpu.cpu_revision = val.parse().ok(),
283            _ => {} // ignore unknown entry
284        },
285        _ => {}
286    }
287}
288
289fn get_bool(s: &str) -> bool {
290    match s {
291        "yes" | "ok" => true,
292        _ => false,
293    }
294}
295
296/// Processor usage statistics
297#[derive(Debug, Deserialize, Serialize, Clone, Default)]
298pub struct Stat {
299    pub cpu: Option<CpuUsage>,
300    pub cpus: Vec<CpuUsage>,
301    pub interrupts: Option<u64>,
302    pub context_switches: Option<u64>,
303    pub boot_time: Option<u64>,
304    pub processes_created: Option<u64>,
305    pub processes_running: Option<u64>,
306    pub processes_blocked: Option<u64>,
307    pub softirq: Option<SoftIrq>,
308}
309
310impl Stat {
311    pub fn new() -> Result<Self> {
312        parse_proc_stat()
313    }
314}
315
316#[derive(Debug, Deserialize, Serialize, Clone, Copy, Default)]
317pub struct CpuUsage {
318    pub user: Option<u64>,
319    pub nice: Option<u64>,
320    pub system: Option<u64>,
321    pub idle: Option<u64>,
322    pub iowait: Option<u64>,
323    pub irq: Option<u64>,
324    pub softirq: Option<u64>,
325    pub steal: Option<u64>,
326    pub guest: Option<u64>,
327    pub guest_nice: Option<u64>,
328}
329
330impl CpuUsage {
331    pub fn total_time(&self) -> u64 {
332        self.user.unwrap_or(0)
333            + self.nice.unwrap_or(0)
334            + self.system.unwrap_or(0)
335            + self.idle.unwrap_or(0)
336            + self.iowait.unwrap_or(0)
337            + self.irq.unwrap_or(0)
338            + self.softirq.unwrap_or(0)
339            + self.steal.unwrap_or(0)
340    }
341
342    pub fn active_time(&self) -> u64 {
343        self.total_time() - self.idle.unwrap_or(0) - self.iowait.unwrap_or(0)
344    }
345
346    pub fn usage_percentage(&self, prev: Option<Self>) -> f32 {
347        if prev.is_none() {
348            return 0.0;
349        }
350        let prev = prev.unwrap();
351
352        let total_diff = self.total_time() - prev.total_time();
353        let active_diff = self.active_time() - prev.active_time();
354
355        if total_diff > 0 {
356            (active_diff as f32 / total_diff as f32) * 100.0
357        } else {
358            0.0
359        }
360    }
361}
362
363impl From<&str> for CpuUsage {
364    fn from(value: &str) -> Self {
365        let parts = value.split_whitespace().collect::<Vec<&str>>();
366        if parts.is_empty() || parts.len() < 11 {
367            return Self::default();
368        }
369
370        Self {
371            user: parts[1].parse().ok(),
372            nice: parts[2].parse().ok(),
373            system: parts[3].parse().ok(),
374            idle: parts[4].parse().ok(),
375            iowait: parts[5].parse().ok(),
376            irq: parts[6].parse().ok(),
377            softirq: parts[7].parse().ok(),
378            steal: parts[8].parse().ok(),
379            guest: parts[9].parse().ok(),
380            guest_nice: parts[10].parse().ok(),
381        }
382    }
383}
384
385#[derive(Debug, Deserialize, Serialize, Clone, Copy, Default)]
386pub struct SoftIrq {
387    pub total: Option<u64>,
388    pub hi: Option<u64>,
389    pub timer: Option<u64>,
390    pub net_tx: Option<u64>,
391    pub net_rx: Option<u64>,
392    pub block: Option<u64>,
393    pub irq_poll: Option<u64>,
394    pub tasklet: Option<u64>,
395    pub shed: Option<u64>,
396    pub hrtimer: Option<u64>,
397    pub rcu: Option<u64>,
398}
399
400impl From<&str> for SoftIrq {
401    fn from(value: &str) -> Self {
402        let parts = value.split_whitespace().collect::<Vec<&str>>();
403        if parts.is_empty() || parts.len() < 12 {
404            return Self::default();
405        }
406
407        Self {
408            total: parts[1].parse().ok(),
409            hi: parts[2].parse().ok(),
410            timer: parts[3].parse().ok(),
411            net_tx: parts[4].parse().ok(),
412            net_rx: parts[5].parse().ok(),
413            block: parts[6].parse().ok(),
414            irq_poll: parts[7].parse().ok(),
415            tasklet: parts[8].parse().ok(),
416            shed: parts[9].parse().ok(),
417            hrtimer: parts[10].parse().ok(),
418            rcu: parts[11].parse().ok(),
419        }
420    }
421}
422
423fn parse_proc_stat() -> Result<Stat> {
424    let content = read_to_string("/proc/stat")?;
425    let mut stat = Stat::default();
426
427    for line in content.lines() {
428        let parts = line.split_whitespace().collect::<Vec<&str>>();
429        if parts.is_empty() {
430            continue;
431        }
432        match parts[0] {
433            "cpu" => {
434                if parts.len() >= 11 {
435                    stat.cpu = Some(CpuUsage::from(line));
436                }
437            }
438            key if key.starts_with("cpu")
439                && key[3..]
440                    .chars()
441                    .next()
442                    .map(|c| c.is_ascii_digit())
443                    .unwrap_or(false) =>
444            {
445                if parts.len() >= 11 {
446                    stat.cpus.push(CpuUsage::from(line));
447                }
448            }
449            "intr" => {
450                if parts.len() >= 2 {
451                    stat.interrupts = parts[1].parse().ok();
452                }
453            }
454            "ctxt" => {
455                if parts.len() >= 2 {
456                    stat.context_switches = parts[1].parse().ok();
457                }
458            }
459            "btime" => {
460                if parts.len() >= 2 {
461                    stat.boot_time = parts[1].parse().ok();
462                }
463            }
464            "processes" => {
465                if parts.len() >= 2 {
466                    stat.processes_created = parts[1].parse().ok();
467                }
468            }
469            "procs_running" => {
470                if parts.len() >= 2 {
471                    stat.processes_running = parts[1].parse().ok();
472                }
473            }
474            "procs_blocked" => {
475                if parts.len() >= 2 {
476                    stat.processes_blocked = parts[1].parse().ok();
477                }
478            }
479            "softirq" => {
480                if parts.len() >= 12 {
481                    stat.softirq = Some(SoftIrq::from(line));
482                }
483            }
484            _ => {}
485        }
486    }
487    Ok(stat)
488}