Skip to main content

ferrix_lib/
cpu.rs

1/* cpu.rs
2 *
3 * Copyright 2025-2026 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, anyhow};
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 = "Information about processors".to_string();
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(not(target_arch = "aarch64"))]
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 => "\nProcessor #unknown\n".to_string(),
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        .map_err(|err| anyhow!("read_info(): Failed to read `/proc/cpuinfo` file: {err}"))?;
212    let blocks = blocks
213        .split("\n\n") // split by CPU blocks
214        .collect::<Vec<_>>();
215    let mut processors = Vec::with_capacity(blocks.len());
216
217    for block in blocks {
218        if block.trim().is_empty() {
219            continue;
220        }
221        let mut cpu = CPU::default();
222        for line in block.lines() {
223            parse_cpuinfo(&mut cpu, line);
224        }
225        processors.push(cpu);
226    }
227    Ok(processors)
228}
229
230fn get_parts(s: &str) -> impl Iterator<Item = &str> {
231    s.splitn(2, ':').map(|item| item.trim())
232}
233
234#[cfg(not(target_arch = "aarch64"))]
235fn parse_cpuinfo(cpu: &mut CPU, parts: &str) {
236    let mut parts = get_parts(parts);
237    if let (Some(key), Some(val)) = (parts.next(), parts.next()) {
238        match key {
239            "processor" => cpu.processor = val.parse().ok(),
240            "vendor_id" => cpu.vendor_id = Some(val.to_string()),
241            "cpu family" => cpu.cpu_family = val.parse().ok(),
242            "model" => cpu.model = val.parse().ok(),
243            "model name" => cpu.model_name = Some(val.to_string()),
244            "stepping" => cpu.stepping = val.parse().ok(),
245            "microcode" => cpu.microcode = Some(val.to_string()),
246            "cpu MHz" => cpu.cpu_mhz = val.parse().ok(),
247            "cache size" => cpu.cache_size = Size::try_from(val).ok(),
248            "physical id" => cpu.physical_id = val.parse().ok(),
249            "siblings" => cpu.siblings = val.parse().ok(),
250            "core id" => cpu.core_id = val.parse().ok(),
251            "cpu cores" => cpu.cpu_cores = val.parse().ok(),
252            "apicid" => cpu.apicid = val.parse().ok(),
253            "initial apicid" => cpu.initial_apicid = val.parse().ok(),
254            "fpu" => cpu.fpu = Some(get_bool(val)),
255            "fpu_exception" => cpu.fpu_exception = Some(get_bool(val)),
256            "cpuid level" => cpu.cpuid_level = val.parse().ok(),
257            "wp" => cpu.wp = Some(get_bool(val)),
258            "flags" | "Features" => {
259                cpu.flags = Some(val.split_whitespace().map(String::from).collect())
260            }
261            "bugs" => cpu.bugs = Some(val.split_whitespace().map(String::from).collect()),
262            "bogomips" | "BogoMIPS" => cpu.bogomips = val.parse().ok(),
263            "clflush size" => cpu.clflush_size = val.parse().ok(),
264            "cache_alignment" => cpu.cache_alignment = val.parse().ok(),
265            "address sizes" => cpu.address_sizes = Some(val.to_string()),
266            "power management" => cpu.power_management = Some(val.to_string()),
267            _ => {} // ignore unknown entry
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    matches!(s, "yes" | "ok")
291}
292
293/// Processor usage statistics
294#[derive(Debug, Deserialize, Serialize, Clone, Default)]
295pub struct Stat {
296    pub cpu: Option<CpuUsage>,
297    pub cpus: Vec<CpuUsage>,
298    pub interrupts: Option<u64>,
299    pub context_switches: Option<u64>,
300    pub boot_time: Option<u64>,
301    pub processes_created: Option<u64>,
302    pub processes_running: Option<u64>,
303    pub processes_blocked: Option<u64>,
304    pub softirq: Option<SoftIrq>,
305}
306
307impl Stat {
308    pub fn new() -> Result<Self> {
309        parse_proc_stat()
310    }
311}
312
313#[derive(Debug, Deserialize, Serialize, Clone, Copy, Default)]
314pub struct CpuUsage {
315    pub user: Option<u64>,
316    pub nice: Option<u64>,
317    pub system: Option<u64>,
318    pub idle: Option<u64>,
319    pub iowait: Option<u64>,
320    pub irq: Option<u64>,
321    pub softirq: Option<u64>,
322    pub steal: Option<u64>,
323    pub guest: Option<u64>,
324    pub guest_nice: Option<u64>,
325}
326
327impl CpuUsage {
328    pub fn total_time(&self) -> u64 {
329        self.user.unwrap_or(0)
330            + self.nice.unwrap_or(0)
331            + self.system.unwrap_or(0)
332            + self.idle.unwrap_or(0)
333            + self.iowait.unwrap_or(0)
334            + self.irq.unwrap_or(0)
335            + self.softirq.unwrap_or(0)
336            + self.steal.unwrap_or(0)
337    }
338
339    pub fn active_time(&self) -> u64 {
340        self.total_time() - self.idle.unwrap_or(0) - self.iowait.unwrap_or(0)
341    }
342
343    pub fn usage_percentage(&self, prev: Option<Self>) -> f32 {
344        if prev.is_none() {
345            return 0.0;
346        }
347        let prev = prev.unwrap();
348
349        let total_diff = self.total_time().wrapping_sub(prev.total_time()); // shitcode (see #50 issue)!
350        let active_diff = self.active_time().wrapping_sub(prev.active_time());
351
352        if total_diff > 0 {
353            (active_diff as f32 / total_diff as f32) * 100.0
354        } else {
355            0.0
356        }
357    }
358}
359
360impl From<&str> for CpuUsage {
361    fn from(value: &str) -> Self {
362        let parts = value.split_whitespace().collect::<Vec<&str>>();
363        if parts.is_empty() || parts.len() < 11 {
364            return Self::default();
365        }
366
367        Self {
368            user: parts[1].parse().ok(),
369            nice: parts[2].parse().ok(),
370            system: parts[3].parse().ok(),
371            idle: parts[4].parse().ok(),
372            iowait: parts[5].parse().ok(),
373            irq: parts[6].parse().ok(),
374            softirq: parts[7].parse().ok(),
375            steal: parts[8].parse().ok(),
376            guest: parts[9].parse().ok(),
377            guest_nice: parts[10].parse().ok(),
378        }
379    }
380}
381
382#[derive(Debug, Deserialize, Serialize, Clone, Copy, Default)]
383pub struct SoftIrq {
384    pub total: Option<u64>,
385    pub hi: Option<u64>,
386    pub timer: Option<u64>,
387    pub net_tx: Option<u64>,
388    pub net_rx: Option<u64>,
389    pub block: Option<u64>,
390    pub irq_poll: Option<u64>,
391    pub tasklet: Option<u64>,
392    pub shed: Option<u64>,
393    pub hrtimer: Option<u64>,
394    pub rcu: Option<u64>,
395}
396
397impl From<&str> for SoftIrq {
398    fn from(value: &str) -> Self {
399        let parts = value.split_whitespace().collect::<Vec<&str>>();
400        if parts.is_empty() || parts.len() < 12 {
401            return Self::default();
402        }
403
404        Self {
405            total: parts[1].parse().ok(),
406            hi: parts[2].parse().ok(),
407            timer: parts[3].parse().ok(),
408            net_tx: parts[4].parse().ok(),
409            net_rx: parts[5].parse().ok(),
410            block: parts[6].parse().ok(),
411            irq_poll: parts[7].parse().ok(),
412            tasklet: parts[8].parse().ok(),
413            shed: parts[9].parse().ok(),
414            hrtimer: parts[10].parse().ok(),
415            rcu: parts[11].parse().ok(),
416        }
417    }
418}
419
420fn parse_proc_stat() -> Result<Stat> {
421    let content = read_to_string("/proc/stat")
422        .map_err(|err| anyhow!("parse_proc_stat(): Failed to read `/proc/stat` file: {err}"))?;
423    let mut stat = Stat::default();
424
425    for line in content.lines() {
426        let parts = line.split_whitespace().collect::<Vec<&str>>();
427        if parts.is_empty() {
428            continue;
429        }
430        match parts[0] {
431            "cpu" => {
432                if parts.len() >= 11 {
433                    stat.cpu = Some(CpuUsage::from(line));
434                }
435            }
436            key if key.starts_with("cpu")
437                && key[3..]
438                    .chars()
439                    .next()
440                    .map(|c| c.is_ascii_digit())
441                    .unwrap_or(false) =>
442            {
443                if parts.len() >= 11 {
444                    stat.cpus.push(CpuUsage::from(line));
445                }
446            }
447            "intr" => {
448                if parts.len() >= 2 {
449                    stat.interrupts = parts[1].parse().ok();
450                }
451            }
452            "ctxt" => {
453                if parts.len() >= 2 {
454                    stat.context_switches = parts[1].parse().ok();
455                }
456            }
457            "btime" => {
458                if parts.len() >= 2 {
459                    stat.boot_time = parts[1].parse().ok();
460                }
461            }
462            "processes" => {
463                if parts.len() >= 2 {
464                    stat.processes_created = parts[1].parse().ok();
465                }
466            }
467            "procs_running" if parts.len() >= 2 => {
468                stat.processes_running = parts[1].parse().ok();
469            }
470            "procs_blocked" if parts.len() >= 2 => {
471                stat.processes_blocked = parts[1].parse().ok();
472            }
473            "softirq" if parts.len() >= 12 => {
474                stat.softirq = Some(SoftIrq::from(line));
475            }
476            _ => {}
477        }
478    }
479    Ok(stat)
480}