Skip to main content

yggdrasil/
sysinfo_sampler.rs

1//! sysinfo 主机指标后台采样。
2//!
3//! sysinfo 的 CPU% **不是即时值**,需要两次采样间的 delta。因此用后台任务周期
4//! 刷新,server function 只读 [`tokio::sync::RwLock`] 快照(毫秒级返回,零采样成本)。
5//!
6//! 采样间隔由环境变量 `SYSINFO_SAMPLE_SECS` 配置(默认 0.5 秒,支持小数如 0.1)。
7
8// LazyLock / Duration 仅 server 构建的采样任务用到;WASM 端只序列化 SystemSnapshot。
9#[cfg(feature = "server")]
10use std::sync::LazyLock;
11#[cfg(feature = "server")]
12use std::time::Duration;
13
14use serde::{Deserialize, Serialize};
15
16/// 主机指标快照(由后台采样任务周期更新)。
17#[derive(Clone, Default, Serialize, Deserialize, Debug)]
18pub struct SystemSnapshot {
19    /// 总体 CPU 使用率(百分比)。
20    pub cpu_usage: f32,
21    /// 系统 1 分钟平均负载。
22    pub load_avg_1: f64,
23    /// 总物理内存(字节)。
24    pub total_memory: u64,
25    /// 已用物理内存(字节)。
26    pub used_memory: u64,
27    /// 主磁盘总空间(字节,取根分区或最大盘)。
28    pub disk_total: u64,
29    /// 主磁盘可用空间(字节)。
30    pub disk_available: u64,
31    /// 操作系统版本(如 "macOS 15.5")。
32    pub os_name: String,
33    /// 内核版本。
34    pub kernel_version: String,
35    /// 系统启动后秒数。
36    pub uptime_secs: u64,
37}
38
39#[cfg(feature = "server")]
40static SNAPSHOT: LazyLock<tokio::sync::RwLock<SystemSnapshot>> =
41    LazyLock::new(|| tokio::sync::RwLock::new(SystemSnapshot::default()));
42
43/// 采样间隔,由环境变量 `SYSINFO_SAMPLE_SECS` 配置,默认 0.5 秒,下限 0.05 秒。
44#[cfg(feature = "server")]
45fn sample_interval() -> Duration {
46    let secs = std::env::var("SYSINFO_SAMPLE_SECS")
47        .ok()
48        .and_then(|s| s.parse::<f64>().ok())
49        .unwrap_or(0.5);
50    Duration::from_secs_f64(secs.max(0.05))
51}
52
53/// 启动后台采样任务。应在 main 启动流程(migrate 之后、serve 之前)调用一次。
54///
55/// CPU% 需两次采样 delta,故循环里先 refresh 再 sleep 再 refresh 才有有效值。
56#[cfg(feature = "server")]
57pub fn spawn_sampler() {
58    tokio::spawn(async move {
59        use sysinfo::{Disks, System};
60
61        let mut sys = System::new();
62        let interval = sample_interval();
63        // 首次 refresh 建立基线,CPU% 在下一轮才有意义。
64        sys.refresh_cpu_usage();
65        let disks = Disks::new_with_refreshed_list();
66
67        loop {
68            tokio::time::sleep(interval).await;
69            sys.refresh_cpu_usage();
70            sys.refresh_memory();
71            let load = System::load_average();
72
73            // 主磁盘:取空间最大的盘(通常是数据盘)。
74            let (disk_total, disk_available) = disks
75                .list()
76                .iter()
77                .max_by_key(|d| d.total_space())
78                .map(|d| (d.total_space(), d.available_space()))
79                .unwrap_or((0, 0));
80
81            let snap = SystemSnapshot {
82                cpu_usage: sys.global_cpu_usage(),
83                load_avg_1: load.one,
84                total_memory: sys.total_memory(),
85                used_memory: sys.used_memory(),
86                disk_total,
87                disk_available,
88                os_name: System::long_os_version().unwrap_or_default(),
89                kernel_version: System::kernel_version().unwrap_or_default(),
90                uptime_secs: System::uptime(),
91            };
92            *SNAPSHOT.write().await = snap;
93        }
94    });
95}
96
97/// 读取最新快照(只读,毫秒级返回,不触发采样)。
98#[cfg(feature = "server")]
99pub async fn read_snapshot() -> SystemSnapshot {
100    SNAPSHOT.read().await.clone()
101}
102
103#[cfg(not(feature = "server"))]
104#[allow(dead_code)]
105pub async fn read_snapshot() -> SystemSnapshot {
106    SystemSnapshot::default()
107}