Files
mqttx/gpui-app/src/sysmon.rs
T
JoeAllen 22e0c70b90 fix(cross-platform): set_topmost/sample 改属性 cfg 分派,修复非 Windows 编译
cfg!() 只是运行时常量,其分支永远参与类型检查 —— 非 Windows 下
sample_windows / set_topmost_windows 被属性 cfg 移除后调用点报
E0599 方法不存在(Linux CI Test job 第二跑暴露)。改为
#[cfg] 属性块分派,两条路径各平台上只有一个参与编译。
2026-09-30 13:12:36 +08:00

230 lines
8.1 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 进程自身资源采样(CPU/内存/线程/句柄)。
//!
//! 仅 Windows 实现数据采集(`windows` crate);其他平台采样返回全零数据,
//! 调用方按缺失能力展示。窗口置顶等平台操作见 [`crate::platform`]。
use std::time::{Duration, Instant};
/// 进程资源的一次采样。
#[derive(Debug, Clone, Default)]
pub struct Sample {
/// 工作集(物理内存占用)
pub working_set: u64,
/// 峰值工作集(本次会话以来最高)
pub peak_working_set: u64,
/// 提交内存(私有字节)
pub private_bytes: u64,
/// 自上次采样以来的 CPU 占用(占整机容量的百分比 0~100,已按核数归一,
/// 与任务管理器的"总利用率"同口径)
pub cpu_percent: f32,
/// 线程数
pub threads: u32,
/// 句柄数(采样失败为 0)
pub handles: u32,
}
/// 周期采样器:内部记录上次 CPU 时间,用于差分计算占用率。
pub struct Sampler {
pid: u32,
last_cpu: Option<(u64, u64, Instant)>,
/// 上一次成功算出的占用率:采样间隔过近时沿用,避免读数跳回 0
last_percent: f32,
}
impl Sampler {
pub fn new() -> Self {
Self {
pid: std::process::id(),
last_cpu: None,
last_percent: 0.0,
}
}
/// 采样一次;CPU 占用首次采样为 0(无差分基准)。
pub fn sample(&mut self) -> Option<Sample> {
// 属性 cfg 分派而非 cfg!() 运行时判断:非 Windows 下 sample_windows
// 被 cfg 移除,cfg! 分支仍参与编译会报方法不存在(CI Test job 实测暴露)
#[cfg(target_os = "windows")]
{
self.sample_windows()
}
#[cfg(not(target_os = "windows"))]
{
Some(Sample::default())
}
}
#[cfg(target_os = "windows")]
fn sample_windows(&mut self) -> Option<Sample> {
use windows::Win32::Foundation::CloseHandle;
use windows::Win32::System::Diagnostics::ToolHelp::{
CreateToolhelp32Snapshot, Thread32First, Thread32Next, TH32CS_SNAPTHREAD,
THREADENTRY32,
};
use windows::Win32::System::ProcessStatus::{
GetProcessMemoryInfo, PROCESS_MEMORY_COUNTERS, PROCESS_MEMORY_COUNTERS_EX,
};
use windows::Win32::System::Threading::{
GetProcessHandleCount, GetProcessTimes, OpenProcess,
PROCESS_QUERY_LIMITED_INFORMATION,
};
use windows::Win32::Foundation::FILETIME;
let process = unsafe {
OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, self.pid)
.ok()?
};
// ── 内存 ──
// windows 0.58 的 EX 结构是扁平的(cb 在首位),按 EX 大小传 cb,
// GetProcessMemoryInfo 即填充 PrivateUsage 扩展字段
let mut mem: PROCESS_MEMORY_COUNTERS_EX = PROCESS_MEMORY_COUNTERS_EX {
cb: std::mem::size_of::<PROCESS_MEMORY_COUNTERS_EX>() as u32,
PageFaultCount: 0,
PeakWorkingSetSize: 0,
WorkingSetSize: 0,
QuotaPeakPagedPoolUsage: 0,
QuotaPagedPoolUsage: 0,
QuotaPeakNonPagedPoolUsage: 0,
QuotaNonPagedPoolUsage: 0,
PagefileUsage: 0,
PeakPagefileUsage: 0,
PrivateUsage: 0,
};
let ok = unsafe {
GetProcessMemoryInfo(
process,
&mut mem as *mut PROCESS_MEMORY_COUNTERS_EX as *mut PROCESS_MEMORY_COUNTERS,
mem.cb,
)
.is_ok()
};
if !ok {
let _ = unsafe { CloseHandle(process) };
return None;
}
// ── CPU 时间差分 ──
let mut creation = FILETIME::default();
let mut exit = FILETIME::default();
let mut kernel = FILETIME::default();
let mut user = FILETIME::default();
let mut cpu_percent = 0f32;
let now = Instant::now();
let have_times = unsafe {
GetProcessTimes(
process,
&mut creation,
&mut exit,
&mut kernel,
&mut user,
)
.is_ok()
};
if have_times {
let k = ((kernel.dwHighDateTime as u64) << 32) | kernel.dwLowDateTime as u64;
let u = ((user.dwHighDateTime as u64) << 32) | user.dwLowDateTime as u64;
let cores = std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1) as f32;
if let Some((pk, pu, pt)) = self.last_cpu {
let dt = now.duration_since(pt).as_secs_f32();
if dt > 0.05 {
let cpu_secs = ((k - pk) + (u - pu)) as f32 / 1e7;
cpu_percent = cpu_secs / dt / cores * 100.;
self.last_percent = cpu_percent;
// 只有真正算出结果才前移差分基准:否则连续两次快速采样
// 会既报 0 又把基准推到"刚刚",用户手点"立即刷新"永远读到 0%
self.last_cpu = Some((k, u, now));
} else {
cpu_percent = self.last_percent;
}
} else {
self.last_cpu = Some((k, u, now));
}
}
// ── 线程数 / 句柄数 ──
let mut threads = 0u32;
unsafe {
if let Ok(snapshot) = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0) {
let mut entry = THREADENTRY32 {
dwSize: std::mem::size_of::<THREADENTRY32>() as u32,
..std::mem::zeroed()
};
if Thread32First(snapshot, &mut entry).is_ok() {
loop {
if entry.th32OwnerProcessID == self.pid {
threads += 1;
}
if Thread32Next(snapshot, &mut entry).is_err() {
break;
}
}
}
let _ = CloseHandle(snapshot);
}
}
let mut handles = 0u32;
unsafe {
let _ = GetProcessHandleCount(process, &mut handles);
}
let _ = unsafe { CloseHandle(process) };
Some(Sample {
working_set: mem.WorkingSetSize as u64,
peak_working_set: mem.PeakWorkingSetSize as u64,
private_bytes: mem.PrivateUsage as u64,
cpu_percent,
threads,
handles,
}) }
}
impl Default for Sampler {
fn default() -> Self {
Self::new()
}
}
static START: std::sync::OnceLock<Instant> = std::sync::OnceLock::new();
/// 进程启动时调用一次(main / 库初始化),用于计算运行时长。
pub fn init() {
let _ = START.set(Instant::now());
}
/// 进程运行时长;未调用 [`init`] 时返回 0。
pub fn uptime() -> Duration {
START.get().map(|t| t.elapsed()).unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sampler_returns_a_sample() {
init();
let mut s = Sampler::new();
let first = s.sample().expect("采样应成功");
std::thread::sleep(Duration::from_millis(30));
let second = s.sample().expect("第二次采样应成功");
// 采样能力分平台:Windows 采集真实数据,其余平台按契约返回全零占位样本。
// 这里用 if cfg! 而不是 #[cfg]:两个分支都参与编译,避免只在某一个平台上
// 才发现语法/类型错误。此前无条件断言 > 0,导致 Linux 上 cargo test 必失败,
// 而 CI 的 test job 正是跑在 ubuntu-latest 上,v* tag 会直接停在测试阶段。
if cfg!(target_os = "windows") {
assert!(second.working_set > 0, "工作集应大于 0");
} else {
assert_eq!(second.working_set, 0, "非 Windows 平台应返回全零占位样本");
}
let _ = (first, second);
}
#[test]
fn uptime_increases() {
init();
std::thread::sleep(Duration::from_millis(10));
assert!(uptime().as_millis() >= 10);
}
}