use std::path::PathBuf;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use crate::process_watcher::sample_system_gpu_mem;
#[derive(Clone)]
pub struct SeriesPoint<T> {
pub timestamp_ms: u64,
pub data: T,
}
#[derive(Clone, Default)]
pub struct SeriesData<T> {
pub points: Vec<SeriesPoint<T>>,
}
impl<T> SeriesData<T> {
pub fn new() -> Self {
SeriesData { points: Vec::new() }
}
pub fn push(&mut self, timestamp_ms: u64, data: T) {
self.points.push(SeriesPoint { timestamp_ms, data });
}
pub fn is_empty(&self) -> bool {
self.points.is_empty()
}
}
impl SeriesData<f64> {
pub fn iter_secs(&self) -> impl Iterator<Item = (f64, f64)> + '_ {
self.points
.iter()
.map(|p| (p.timestamp_ms as f64 / 1000.0, p.data))
}
pub fn max_value(&self) -> f64 {
self.points.iter().map(|p| p.data).fold(0.0f64, f64::max)
}
}
pub struct MonitorData {
pub cpu_pct: SeriesData<f64>,
pub mem_available_mb: SeriesData<f64>,
pub swap_used_mb: SeriesData<f64>,
pub active_replays: SeriesData<f64>,
pub active_downloads: SeriesData<f64>,
pub gpu_mem_mb: SeriesData<f64>,
pub gpu_freq_mhz: SeriesData<f64>,
pub gpu_freq_max_mhz: Option<u32>,
pub gpu_mem_freq_mhz: SeriesData<f64>,
pub gpu_busy_pct: SeriesData<f64>,
pub cpu_temp_c: SeriesData<f64>,
pub gpu_temp_c: SeriesData<f64>,
}
impl MonitorData {
pub fn new(gpu_freq_max_mhz: Option<u32>) -> Self {
MonitorData {
cpu_pct: SeriesData::new(),
mem_available_mb: SeriesData::new(),
swap_used_mb: SeriesData::new(),
active_replays: SeriesData::new(),
active_downloads: SeriesData::new(),
gpu_mem_mb: SeriesData::new(),
gpu_freq_mhz: SeriesData::new(),
gpu_freq_max_mhz,
gpu_mem_freq_mhz: SeriesData::new(),
gpu_busy_pct: SeriesData::new(),
cpu_temp_c: SeriesData::new(),
gpu_temp_c: SeriesData::new(),
}
}
pub fn duration_ms(&self) -> u64 {
self.cpu_pct
.points
.iter()
.chain(self.gpu_busy_pct.points.iter())
.map(|p| p.timestamp_ms)
.max()
.unwrap_or(0)
}
}
impl Default for MonitorData {
fn default() -> Self {
MonitorData::new(None)
}
}
pub struct MonitorTimeRange {
pub start: u64,
pub end: u64,
}
impl MonitorTimeRange {
pub fn new(start: Duration, end: Duration) -> Self {
Self {
start: start.as_millis() as u64,
end: end.as_millis() as u64,
}
}
}
#[derive(Clone, Debug)]
pub struct TraceEvent {
pub trace_name: String,
pub download_start_ms: u64,
pub download_end_ms: u64,
pub replay_start_ms: u64,
pub replay_end_ms: u64,
pub passed: bool,
}
struct GpuPaths {
freedreno_gem: Option<PathBuf>,
freedreno_perf: Option<PathBuf>,
freedreno_cur_freq: Option<PathBuf>,
freedreno_max_freq_mhz: Option<u32>,
amdgpu_freq1_input: Option<PathBuf>,
amdgpu_freq2_input: Option<PathBuf>,
amdgpu_max_freq_mhz: Option<u32>,
i915_gem_objects: Option<PathBuf>,
i915_frequency_info: Option<PathBuf>,
cpu_temp_path: Option<PathBuf>,
gpu_temp_path: Option<PathBuf>,
}
impl GpuPaths {
fn detect() -> Self {
let dri = std::path::Path::new("/sys/kernel/debug/dri");
let mut freedreno_gem = None;
let mut freedreno_perf = None;
let mut i915_gem_objects = None;
let mut i915_frequency_info = None;
if let Ok(entries) = std::fs::read_dir(dri) {
for entry in entries.flatten() {
let path = entry.path();
let name_path = path.join("name");
if let Ok(name) = std::fs::read_to_string(&name_path) {
if name.starts_with("msm ") {
let gem = path.join("gem");
if gem.exists() {
freedreno_gem = Some(gem);
}
let perf = path.join("perf");
if perf.exists() {
freedreno_perf = Some(perf);
}
}
}
let gem_obj = path.join("i915_gem_objects");
if gem_obj.exists() {
i915_gem_objects = Some(gem_obj);
}
let freq = path.join("i915_frequency_info");
if freq.exists() {
i915_frequency_info = Some(freq);
}
}
}
let mut freedreno_cur_freq = None;
let mut freedreno_max_freq_mhz = None;
if let Ok(entries) = std::fs::read_dir("/sys/class/devfreq") {
for entry in entries.flatten() {
let path = entry.path();
if let Ok(bytes) = std::fs::read(path.join("device/of_node/compatible")) {
let is_adreno = bytes
.split(|&b| b == 0)
.filter_map(|s| std::str::from_utf8(s).ok())
.any(|s| s.contains("qcom,adreno"));
if is_adreno {
let cur = path.join("cur_freq");
if cur.exists() {
freedreno_cur_freq = Some(cur);
}
freedreno_max_freq_mhz = std::fs::read_to_string(path.join("max_freq"))
.ok()
.and_then(|s| parse_devfreq_hz(s.trim()));
break;
}
}
}
}
let mut amdgpu_freq1_input = None;
let mut amdgpu_freq2_input = None;
let mut amdgpu_max_freq_mhz = None;
if let Ok(entries) = std::fs::read_dir("/sys/class/hwmon") {
for entry in entries.flatten() {
let path = entry.path();
if std::fs::read_to_string(path.join("name"))
.map(|n| n.trim() == "amdgpu")
.unwrap_or(false)
{
let f1 = path.join("freq1_input");
if f1.exists() {
amdgpu_freq1_input = Some(f1);
}
let f2 = path.join("freq2_input");
if f2.exists() {
amdgpu_freq2_input = Some(f2);
}
amdgpu_max_freq_mhz = std::fs::read_to_string(path.join("freq1_max"))
.ok()
.and_then(|s| parse_devfreq_hz(s.trim()));
break;
}
}
}
let (cpu_temp_path, gpu_temp_path) = detect_thermal_zones();
GpuPaths {
freedreno_gem,
freedreno_perf,
freedreno_cur_freq,
freedreno_max_freq_mhz,
amdgpu_freq1_input,
amdgpu_freq2_input,
amdgpu_max_freq_mhz,
i915_gem_objects,
i915_frequency_info,
cpu_temp_path,
gpu_temp_path,
}
}
fn sample_gpu_mem(&self) -> Option<u64> {
if let Some(ref path) = self.freedreno_gem {
if let Ok(content) = std::fs::read_to_string(path) {
if let Some(v) = parse_freedreno_gem(&content) {
return Some(v);
}
}
}
if let Some(ref path) = self.i915_gem_objects {
if let Ok(content) = std::fs::read_to_string(path) {
if let Some(v) = parse_i915_gem_objects(&content) {
return Some(v);
}
}
}
None
}
fn sample_temps(&self) -> (Option<f32>, Option<f32>) {
let cpu = self.cpu_temp_path.as_deref().and_then(read_millicelsius);
let gpu = self.gpu_temp_path.as_deref().and_then(read_millicelsius);
(cpu, gpu)
}
fn sample_gpu_freq(&self) -> (Option<u32>, Option<u32>, Option<u32>) {
if let Some(ref path) = self.freedreno_cur_freq {
if let Ok(content) = std::fs::read_to_string(path) {
if let Some(mhz) = parse_devfreq_hz(content.trim()) {
return (Some(mhz), self.freedreno_max_freq_mhz, None);
}
}
}
if self.amdgpu_freq1_input.is_some() || self.amdgpu_freq2_input.is_some() {
let gfx = self
.amdgpu_freq1_input
.as_deref()
.and_then(|p| std::fs::read_to_string(p).ok())
.and_then(|s| parse_devfreq_hz(s.trim()));
let mem = self
.amdgpu_freq2_input
.as_deref()
.and_then(|p| std::fs::read_to_string(p).ok())
.and_then(|s| parse_devfreq_hz(s.trim()));
return (gfx, self.amdgpu_max_freq_mhz, mem);
}
let path = match &self.i915_frequency_info {
Some(p) => p,
None => return (None, None, None),
};
match std::fs::read_to_string(path) {
Ok(content) => {
let (actual, max) = parse_i915_frequency_info(&content);
(actual, max, None)
}
Err(_) => (None, None, None),
}
}
}
fn detect_thermal_zones() -> (Option<PathBuf>, Option<PathBuf>) {
let Ok(entries) = std::fs::read_dir("/sys/class/thermal") else {
return (None, None);
};
let mut zones: Vec<_> = entries.flatten().collect();
zones.sort_by_key(|e| e.file_name());
let mut cpu = None;
let mut gpu = None;
for entry in zones {
let path = entry.path();
let Ok(type_str) = std::fs::read_to_string(path.join("type")) else {
continue;
};
let t = type_str.trim();
if cpu.is_none() && t == "x86_pkg_temp" {
cpu = Some(path.join("temp"));
} else if cpu.is_none() && t == "acpitz" {
let acpi_path = std::fs::read_to_string(path.join("device/path")).unwrap_or_default();
if acpi_path.trim() == r"\_TZ_.CPUZ" {
cpu = Some(path.join("temp"));
}
}
if gpu.is_none() && t.contains("gpu") {
gpu = Some(path.join("temp"));
} else if gpu.is_none() && t == "acpitz" {
let acpi_path = std::fs::read_to_string(path.join("device/path")).unwrap_or_default();
if acpi_path.trim() == r"\_TZ_.GFXZ" {
gpu = Some(path.join("temp"));
}
}
}
(cpu, gpu)
}
fn read_millicelsius(path: &std::path::Path) -> Option<f32> {
let s = std::fs::read_to_string(path).ok()?;
let mc: i32 = s.trim().parse().ok()?;
Some(mc as f32 / 1000.0)
}
fn parse_devfreq_hz(s: &str) -> Option<u32> {
s.parse::<u64>().ok().map(|hz| (hz / 1_000_000) as u32)
}
fn parse_freedreno_gem(content: &str) -> Option<u64> {
for line in content.lines() {
if let Some(rest) = line.strip_prefix("Total:") {
if let Some(bytes_part) = rest.split(',').nth(1) {
let s = bytes_part.trim().trim_end_matches(" bytes");
if let Ok(v) = s.parse::<u64>() {
return Some(v);
}
}
}
}
None
}
fn parse_i915_gem_objects(content: &str) -> Option<u64> {
for line in content.lines() {
if let Some(rest) = line.strip_prefix("system: total:") {
let hex = rest.split_whitespace().next().unwrap_or("");
let hex = hex.trim_start_matches("0x");
if let Ok(v) = u64::from_str_radix(hex, 16) {
return Some(v);
}
}
}
None
}
fn parse_i915_frequency_info(content: &str) -> (Option<u32>, Option<u32>) {
let mut actual = None;
let mut max = None;
for line in content.lines() {
if let Some(rest) = line.strip_prefix("Actual freq:") {
actual = rest.trim().trim_end_matches(" MHz").parse().ok();
}
if let Some(rest) = line.strip_prefix("Max overclocked frequency:") {
max = rest.trim().trim_end_matches("MHz").parse().ok();
}
}
(actual, max)
}
#[derive(Default, Clone)]
struct CpuStat {
total: u64,
idle: u64,
}
fn read_cpu_stat() -> Option<CpuStat> {
let content = std::fs::read_to_string("/proc/stat").ok()?;
let line = content.lines().next()?;
let mut parts = line.split_whitespace().skip(1); let user: u64 = parts.next()?.parse().ok()?;
let nice: u64 = parts.next()?.parse().ok()?;
let system: u64 = parts.next()?.parse().ok()?;
let idle: u64 = parts.next()?.parse().ok()?;
let iowait: u64 = parts.next()?.parse().ok()?;
let irq: u64 = parts.next()?.parse().ok()?;
let softirq: u64 = parts.next()?.parse().ok()?;
let total = user + nice + system + idle + iowait + irq + softirq;
Some(CpuStat { total, idle })
}
fn read_meminfo() -> (u64, u64) {
match std::fs::read_to_string("/proc/meminfo") {
Ok(content) => parse_meminfo(&content),
Err(_) => (0, 0),
}
}
fn parse_meminfo(content: &str) -> (u64, u64) {
let mut available_kb: u64 = 0;
let mut swap_total_kb: u64 = 0;
let mut swap_free_kb: u64 = 0;
for line in content.lines() {
if let Some(rest) = line.strip_prefix("MemAvailable:") {
available_kb = rest.trim().trim_end_matches(" kB").parse().unwrap_or(0);
} else if let Some(rest) = line.strip_prefix("SwapTotal:") {
swap_total_kb = rest.trim().trim_end_matches(" kB").parse().unwrap_or(0);
} else if let Some(rest) = line.strip_prefix("SwapFree:") {
swap_free_kb = rest.trim().trim_end_matches(" kB").parse().unwrap_or(0);
}
}
let swap_used_kb = swap_total_kb.saturating_sub(swap_free_kb);
(available_kb / 1024, swap_used_kb / 1024)
}
#[derive(Default)]
pub struct ActivityCounters {
pub active_replays: AtomicUsize,
pub active_downloads: AtomicUsize,
}
pub struct SystemMonitor {
pub counters: Arc<ActivityCounters>,
pub events: Arc<Mutex<Vec<TraceEvent>>>,
data: Arc<Mutex<MonitorData>>,
stop: Arc<std::sync::atomic::AtomicBool>,
thread: Option<std::thread::JoinHandle<()>>,
}
impl SystemMonitor {
pub fn new() -> Self {
let counters = Arc::new(ActivityCounters::default());
let events = Arc::new(Mutex::new(Vec::new()));
let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
let start = Instant::now();
let gpu = GpuPaths::detect();
let freedreno_perf_path = gpu.freedreno_perf.clone();
let gpu_freq_max_mhz = gpu.freedreno_max_freq_mhz.or(gpu.amdgpu_max_freq_mhz);
let data = Arc::new(Mutex::new(MonitorData::new(gpu_freq_max_mhz)));
if let Some(perf_path) = freedreno_perf_path {
log::debug!("freedreno GPU busy: reading from {perf_path:?}");
let data_t = data.clone();
let stop_t = stop.clone();
let start_t = start;
std::thread::spawn(move || {
use std::io::BufRead;
let file = match std::fs::File::open(&perf_path) {
Ok(f) => f,
Err(e) => {
log::warn!("Failed to open GPU perf file {perf_path:?}: {e}");
return;
}
};
let reader = std::io::BufReader::new(file);
for line in reader.lines() {
if stop_t.load(Ordering::Relaxed) {
break;
}
let line = match line {
Ok(l) => l,
Err(e) => {
log::warn!("Error reading GPU perf file: {e}");
break;
}
};
let trimmed = line.trim();
if let Some(pct) = trimmed
.strip_suffix('%')
.and_then(|s| s.parse::<f64>().ok())
{
let ts = start_t.elapsed().as_millis() as u64;
data_t.lock().unwrap().gpu_busy_pct.push(ts, pct);
}
}
});
} else {
log::debug!("freedreno GPU busy: no perf file found under /sys/kernel/debug/dri/");
}
let counters_t = counters.clone();
let data_t = data.clone();
let stop_t = stop.clone();
let start_t = start;
let thread = std::thread::spawn(move || {
let mut prev_cpu = read_cpu_stat().unwrap_or_default();
loop {
std::thread::sleep(Duration::from_secs(1));
if stop_t.load(Ordering::Relaxed) {
break;
}
let timestamp_ms = start_t.elapsed().as_millis() as u64;
let cur_cpu = read_cpu_stat().unwrap_or_default();
let cpu_pct = {
let dtotal = cur_cpu.total.saturating_sub(prev_cpu.total);
let didle = cur_cpu.idle.saturating_sub(prev_cpu.idle);
if dtotal > 0 {
(100.0 * (dtotal - didle) as f64) / dtotal as f64
} else {
0.0
}
};
prev_cpu = cur_cpu;
let (mem_available_mb, swap_used_mb) = read_meminfo();
let gpu_mem_bytes =
gpu.sample_gpu_mem()
.or_else(|| match sample_system_gpu_mem() {
Some(peak) => {
if peak.vram_bytes.is_none() && peak.sys_bytes.is_none() {
None
} else {
Some(peak.vram_bytes.unwrap_or(0) + peak.sys_bytes.unwrap_or(0))
}
}
None => None,
});
let (gpu_freq_mhz, _, gpu_mem_freq_mhz) = gpu.sample_gpu_freq();
let (cpu_temp_c, gpu_temp_c) = gpu.sample_temps();
let mut data = data_t.lock().unwrap();
data.cpu_pct.push(timestamp_ms, cpu_pct);
data.mem_available_mb
.push(timestamp_ms, mem_available_mb as f64);
data.swap_used_mb.push(timestamp_ms, swap_used_mb as f64);
data.active_replays.push(
timestamp_ms,
counters_t.active_replays.load(Ordering::Relaxed) as f64,
);
data.active_downloads.push(
timestamp_ms,
counters_t.active_downloads.load(Ordering::Relaxed) as f64,
);
if let Some(bytes) = gpu_mem_bytes {
data.gpu_mem_mb
.push(timestamp_ms, bytes as f64 / (1024.0 * 1024.0));
}
if let Some(mhz) = gpu_freq_mhz {
data.gpu_freq_mhz.push(timestamp_ms, mhz as f64);
}
if let Some(mhz) = gpu_mem_freq_mhz {
data.gpu_mem_freq_mhz.push(timestamp_ms, mhz as f64);
}
if let Some(c) = cpu_temp_c {
data.cpu_temp_c.push(timestamp_ms, c as f64);
}
if let Some(c) = gpu_temp_c {
data.gpu_temp_c.push(timestamp_ms, c as f64);
}
}
});
SystemMonitor {
counters,
events,
data,
stop,
thread: Some(thread),
}
}
pub fn stop(mut self) -> (MonitorData, Vec<TraceEvent>) {
self.stop.store(true, Ordering::Relaxed);
if let Some(t) = self.thread.take() {
let _ = t.join();
}
let data = std::mem::take(&mut *self.data.lock().unwrap());
let events = std::mem::take(&mut *self.events.lock().unwrap());
(data, events)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_freedreno_gem_parse() {
let content = "Pinned: 0 objects, 0 bytes\nTotal: 23 objects, 43466752 bytes\n";
assert_eq!(parse_freedreno_gem(content), Some(43466752));
}
#[test]
fn test_i915_gem_objects_parse() {
let content = "system: total:0x0000000faa710000 bytes, shrinkable:0x0 bytes, ...\n";
assert_eq!(parse_i915_gem_objects(content), Some(0x0000000faa710000u64));
}
#[test]
fn test_i915_frequency_parse() {
let content = "Actual freq: 350 MHz\nMax overclocked frequency: 1200MHz\n";
assert_eq!(parse_i915_frequency_info(content), (Some(350), Some(1200)));
}
#[test]
fn test_meminfo_parse() {
let content =
"MemAvailable: 2048000 kB\nSwapTotal: 1048576 kB\nSwapFree: 524288 kB\n";
assert_eq!(parse_meminfo(content), (2000, 512));
}
#[test]
fn test_devfreq_hz_parse() {
assert_eq!(parse_devfreq_hz("700000000"), Some(700));
assert_eq!(parse_devfreq_hz("1000000000"), Some(1000));
assert_eq!(parse_devfreq_hz(""), None);
}
}