use std::sync::OnceLock;
use crate::scheduler::Instant;
#[derive(Debug, Clone)]
pub struct GpuInfo {
pub name: String,
pub backend: String,
}
static GPU_INFO: OnceLock<GpuInfo> = OnceLock::new();
pub fn set_gpu_info(name: impl Into<String>, backend: impl Into<String>) {
_ = GPU_INFO.set(GpuInfo {
name: name.into(),
backend: backend.into(),
});
}
pub fn gpu_info() -> Option<&'static GpuInfo> {
GPU_INFO.get()
}
pub fn install_crash_hook(dir: impl AsRef<std::path::Path>) {
use std::sync::OnceLock;
static HOOK_DIR: OnceLock<std::path::PathBuf> = OnceLock::new();
if HOOK_DIR.set(dir.as_ref().to_path_buf()).is_err() {
return;
}
let previous = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
if let Some(dir) = HOOK_DIR.get() {
_ = std::fs::create_dir_all(dir);
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let mut log = format!("payload: {}\n", panic_payload(info));
log.push_str(&format!(
"location: {}\n",
info.location()
.map(|l| l.to_string())
.unwrap_or_else(|| "未知".to_string())
));
log.push_str(&format!(
"backtrace:\n{}\n",
std::backtrace::Backtrace::force_capture()
));
_ = std::fs::write(dir.join(format!("panic-{nanos}.log")), log);
}
previous(info);
}));
}
fn panic_payload(info: &std::panic::PanicHookInfo) -> String {
if let Some(s) = info.payload().downcast_ref::<&str>() {
s.to_string()
} else if let Some(s) = info.payload().downcast_ref::<String>() {
s.clone()
} else {
"(非文本负载)".to_string()
}
}
pub(crate) struct ProcessSample {
pub(crate) cpu_percent: f64,
pub(crate) memory_mb: f64,
}
#[derive(Default)]
pub(crate) struct RuntimeSamplerState {
pub(crate) clock: CpuClock,
pub(crate) last_sample: Option<Instant>,
pub(crate) last_frame: Option<Instant>,
}
impl Default for CpuClock {
fn default() -> Self {
Self::new()
}
}
pub(crate) struct CpuClock {
#[cfg(target_os = "windows")]
prev_proc_100ns: u64,
#[cfg(unix)]
prev_proc_micros: i64,
prev_wall: Instant,
initialized: bool,
num_cpus: f64,
}
impl CpuClock {
pub(crate) fn new() -> Self {
Self {
#[cfg(target_os = "windows")]
prev_proc_100ns: 0,
#[cfg(unix)]
prev_proc_micros: 0,
prev_wall: Instant::now(),
initialized: false,
num_cpus: std::thread::available_parallelism()
.map(|n| n.get() as f64)
.unwrap_or(1.0),
}
}
pub(crate) fn sample(&mut self) -> Option<ProcessSample> {
let now = Instant::now();
let wall_secs = now.duration_since(self.prev_wall).as_secs_f64();
self.prev_wall = now;
if wall_secs <= 0.0 {
return None;
}
#[cfg(target_os = "windows")]
{
use windows::Win32::Foundation::FILETIME;
use windows::Win32::System::ProcessStatus::{
GetProcessMemoryInfo, PROCESS_MEMORY_COUNTERS_EX,
};
use windows::Win32::System::Threading::{GetCurrentProcess, GetProcessTimes};
unsafe {
let process = GetCurrentProcess();
let mut creation = FILETIME::default();
let mut exit = FILETIME::default();
let mut kernel = FILETIME::default();
let mut user = FILETIME::default();
GetProcessTimes(process, &mut creation, &mut exit, &mut kernel, &mut user).ok()?;
let to_u64 =
|ft: FILETIME| (ft.dwLowDateTime as u64) | ((ft.dwHighDateTime as u64) << 32);
let proc_100ns = to_u64(kernel) + to_u64(user);
let mut counters = PROCESS_MEMORY_COUNTERS_EX::default();
counters.cb = std::mem::size_of::<PROCESS_MEMORY_COUNTERS_EX>() as u32;
GetProcessMemoryInfo(process, &mut counters as *mut _ as *mut _, counters.cb)
.ok()?;
let memory_mb = counters.WorkingSetSize as f64 / (1024.0 * 1024.0);
if !self.initialized {
self.initialized = true;
self.prev_proc_100ns = proc_100ns;
return None;
}
let cpu_percent =
(proc_100ns - self.prev_proc_100ns) as f64 / 1e7 / wall_secs / self.num_cpus
* 100.0;
self.prev_proc_100ns = proc_100ns;
Some(ProcessSample {
cpu_percent: cpu_percent.max(0.0),
memory_mb,
})
}
}
#[cfg(unix)]
{
let mut usage = std::mem::MaybeUninit::<libc::rusage>::zeroed();
if unsafe { libc::getrusage(libc::RUSAGE_SELF, usage.as_mut_ptr()) } != 0 {
return None;
}
let usage = unsafe { usage.assume_init() };
#[cfg(not(target_os = "macos"))]
let to_micros = |tv: libc::timeval| tv.tv_sec * 1_000_000 + tv.tv_usec;
#[cfg(target_os = "macos")]
let to_micros = |tv: libc::timeval| tv.tv_sec * 1_000_000 + i64::from(tv.tv_usec);
let proc_micros = to_micros(usage.ru_utime) + to_micros(usage.ru_stime);
#[cfg(target_os = "macos")]
let memory_mb = usage.ru_maxrss as f64 / (1024.0 * 1024.0);
#[cfg(not(target_os = "macos"))]
let memory_mb = usage.ru_maxrss as f64 / 1024.0;
if !self.initialized {
self.initialized = true;
self.prev_proc_micros = proc_micros;
return None;
}
let cpu_percent = (proc_micros - self.prev_proc_micros) as f64
/ 1_000_000.0
/ wall_secs
/ self.num_cpus
* 100.0;
self.prev_proc_micros = proc_micros;
Some(ProcessSample {
cpu_percent: cpu_percent.max(0.0),
memory_mb,
})
}
#[cfg(not(any(target_os = "windows", unix)))]
{
let _ = (self.initialized, self.num_cpus, now, wall_secs);
self.initialized = true;
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[crate::test]
fn error_ring_caps_at_fifty(cx: &mut crate::TestAppContext) {
cx.update(|cx| {
for i in 0..55 {
cx.report_error(format!("boom-{i}"));
}
let errors = cx.recent_errors();
assert_eq!(errors.len(), 50);
assert_eq!(errors.first().unwrap().0, 5);
assert_eq!(errors.last().unwrap().0, 54);
assert_eq!(errors.last().unwrap().1.to_string(), "boom-54");
});
}
#[test]
fn crash_hook_writes_panic_log() {
let dir = std::env::temp_dir().join(format!(
"rgpui-crash-hook-test-{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
install_crash_hook(&dir);
let _ = std::panic::catch_unwind(|| panic!("hook-探针"));
let mut found = false;
if let Ok(entries) = std::fs::read_dir(&dir) {
for entry in entries.flatten() {
if let Ok(content) = std::fs::read_to_string(entry.path()) {
if content.contains("hook-探针") {
found = true;
}
}
}
}
_ = std::fs::remove_dir_all(&dir);
assert!(found, "panic 日志应包含负载文本");
}
}