use super::adapter::GpuDeviceType;
use alloc::collections::VecDeque;
use core::time::Duration;
use std::time::Instant;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PerformanceMonitorStrategy {
GpuTimestamp,
FrameTime,
CpuUtilization,
}
impl PerformanceMonitorStrategy {
pub fn for_device_type(device_type: GpuDeviceType) -> Self {
match device_type {
GpuDeviceType::DiscreteGpu => Self::GpuTimestamp,
GpuDeviceType::IntegratedGpu => Self::FrameTime,
_ => Self::CpuUtilization,
}
}
}
#[derive(Debug, Clone)]
pub struct AdaptivePerformanceThresholds {
pub target_fps: f32,
pub degrade_threshold: f32,
pub upgrade_threshold: f32,
pub degrade_frame_count: usize,
pub upgrade_frame_count: usize,
pub memory_pressure_threshold: f32,
pub cpu_utilization_threshold: f32,
}
impl AdaptivePerformanceThresholds {
pub fn discrete() -> Self {
Self {
target_fps: 60.0,
degrade_threshold: 1.5, upgrade_threshold: 0.7, degrade_frame_count: 5, upgrade_frame_count: 10, memory_pressure_threshold: 0.9,
cpu_utilization_threshold: 0.8,
}
}
pub fn integrated() -> Self {
Self {
target_fps: 60.0,
degrade_threshold: 1.3, upgrade_threshold: 0.75,
degrade_frame_count: 3, upgrade_frame_count: 15, memory_pressure_threshold: 0.75, cpu_utilization_threshold: 0.7,
}
}
pub fn cpu() -> Self {
Self {
target_fps: 30.0, degrade_threshold: 1.2, upgrade_threshold: 0.8,
degrade_frame_count: 2, upgrade_frame_count: 20, memory_pressure_threshold: 0.6,
cpu_utilization_threshold: 0.5, }
}
pub fn for_device_type(device_type: GpuDeviceType) -> Self {
match device_type {
GpuDeviceType::DiscreteGpu => Self::discrete(),
GpuDeviceType::IntegratedGpu => Self::integrated(),
_ => Self::cpu(),
}
}
pub fn target_frame_duration(&self) -> Duration {
Duration::from_secs_f32(1.0 / self.target_fps)
}
pub fn degrade_duration(&self) -> Duration {
Duration::from_secs_f32(self.target_frame_duration().as_secs_f32() * self.degrade_threshold)
}
pub fn upgrade_duration(&self) -> Duration {
Duration::from_secs_f32(self.target_frame_duration().as_secs_f32() * self.upgrade_threshold)
}
pub fn adjust_based_on_performance(&mut self, avg_frame_time: Duration, stability: f32) {
if stability < 0.5 {
self.degrade_threshold *= 0.9;
self.upgrade_threshold *= 1.1;
}
let avg_fps = 1.0 / avg_frame_time.as_secs_f32();
if avg_fps < self.target_fps * 0.5 {
self.target_fps *= 0.9;
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct PerformanceSample {
pub frame_index: u64,
pub frame_duration: Duration,
pub gpu_time: Option<Duration>,
pub cpu_time: Duration,
pub memory_utilization: f32,
pub cpu_utilization: f32,
pub timestamp: Instant,
}
pub struct AdaptivePerformanceMonitor {
strategy: PerformanceMonitorStrategy,
thresholds: AdaptivePerformanceThresholds,
samples: VecDeque<PerformanceSample>,
max_samples: usize,
current_frame: u64,
frame_start: Instant,
consecutive_bad_frames: usize,
consecutive_good_frames: usize,
last_quality_change: Instant,
}
impl AdaptivePerformanceMonitor {
pub fn for_device_type(device_type: GpuDeviceType) -> Self {
let strategy = PerformanceMonitorStrategy::for_device_type(device_type);
let thresholds = AdaptivePerformanceThresholds::for_device_type(device_type);
Self::new(strategy, thresholds)
}
pub fn new(
strategy: PerformanceMonitorStrategy,
thresholds: AdaptivePerformanceThresholds,
) -> Self {
Self {
strategy,
thresholds,
samples: VecDeque::with_capacity(120),
max_samples: 120,
current_frame: 0,
frame_start: Instant::now(),
consecutive_bad_frames: 0,
consecutive_good_frames: 0,
last_quality_change: Instant::now() - Duration::from_secs(60),
}
}
pub fn begin_frame(&mut self) {
self.frame_start = Instant::now();
self.current_frame += 1;
}
pub fn end_frame(&mut self) -> PerformanceSample {
let frame_duration = self.frame_start.elapsed();
let sample = PerformanceSample {
frame_index: self.current_frame,
frame_duration,
gpu_time: self.measure_gpu_time(),
cpu_time: frame_duration, memory_utilization: self.measure_memory_utilization(),
cpu_utilization: self.measure_cpu_utilization(),
timestamp: Instant::now(),
};
self.record_sample(sample);
sample
}
fn measure_gpu_time(&self) -> Option<Duration> {
if let Ok(val) = std::env::var("RUST_WIDGETS_GPU_TIME_MS") {
if let Ok(ms) = val.trim().parse::<f64>() {
return Some(Duration::from_secs_f64(ms / 1000.0));
}
log::warn!("[performance] RUST_WIDGETS_GPU_TIME_MS value '{}' is not a valid f64", val);
}
log::debug!(
"[performance] measure_gpu_time: no GPU query backend available (strategy={:?})",
self.strategy
);
None
}
fn measure_memory_utilization(&self) -> f32 {
if let Ok(val) = std::env::var("RUST_WIDGETS_MEM_UTIL") {
if let Ok(v) = val.trim().parse::<f32>() {
return v.clamp(0.0, 1.0);
}
log::warn!("[performance] RUST_WIDGETS_MEM_UTIL value '{}' is not a valid f32", val);
}
#[cfg(target_os = "linux")]
{
if let Ok(status) = std::fs::read_to_string("/proc/self/status") {
let mut vmrss_kb: u64 = 0;
let mut vmsize_kb: u64 = 0;
for line in status.lines() {
if let Some(rest) = line.strip_prefix("VmRSS:") {
vmrss_kb = rest.trim().trim_end_matches("kB").trim().parse().unwrap_or(0);
} else if let Some(rest) = line.strip_prefix("VmSize:") {
vmsize_kb = rest.trim().trim_end_matches("kB").trim().parse().unwrap_or(0);
}
}
if vmsize_kb > 0 {
let ratio = vmrss_kb as f32 / vmsize_kb as f32;
log::debug!(
"[performance] memory utilization from /proc/self/status: {ratio:.3}"
);
return ratio.clamp(0.0, 1.0);
}
}
}
#[cfg(target_os = "macos")]
{
let pid = std::process::id().to_string();
if let Ok(output) =
std::process::Command::new("ps").args(["-o", "rss=", "-p", &pid]).output()
{
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
if let Ok(rss_kb) = stdout.trim().parse::<f64>() {
let ratio = (rss_kb / (8.0 * 1024.0 * 1024.0)) as f32;
log::debug!("[performance] memory utilization from ps: {ratio:.3}");
return ratio.clamp(0.0, 1.0);
}
}
}
}
log::debug!("[performance] measure_memory_utilization: no backend available");
0.0
}
fn measure_cpu_utilization(&self) -> f32 {
if let Ok(val) = std::env::var("RUST_WIDGETS_CPU_UTIL") {
if let Ok(v) = val.trim().parse::<f32>() {
return v.clamp(0.0, 1.0);
}
log::warn!("[performance] RUST_WIDGETS_CPU_UTIL value '{}' is not a valid f32", val);
}
#[cfg(target_os = "linux")]
{
if let Ok(status) = std::fs::read_to_string("/proc/self/status") {
for line in status.lines() {
if let Some(rest) = line.strip_prefix("Threads:") {
if let Ok(threads) = rest.trim().parse::<f32>() {
let cores = std::thread::available_parallelism()
.map(|n| n.get() as f32)
.unwrap_or(4.0);
let ratio = (threads / (cores * 2.0)).clamp(0.0, 1.0);
log::debug!(
"[performance] CPU utilization from /proc/self/status: {} threads / {} cores = {ratio:.3}",
threads, cores
);
return ratio;
}
}
}
}
}
log::debug!("[performance] measure_cpu_utilization: no backend available");
0.0
}
fn record_sample(&mut self, sample: PerformanceSample) {
if self.samples.len() >= self.max_samples {
self.samples.pop_front();
}
self.samples.push_back(sample);
if self.is_frame_bad(&sample) {
self.consecutive_bad_frames += 1;
self.consecutive_good_frames = 0;
} else {
self.consecutive_good_frames += 1;
self.consecutive_bad_frames = 0;
}
}
fn is_frame_bad(&self, sample: &PerformanceSample) -> bool {
sample.frame_duration > self.thresholds.degrade_duration()
}
pub fn should_degrade(&self) -> bool {
if self.consecutive_bad_frames >= self.thresholds.degrade_frame_count {
if self.last_quality_change.elapsed() > Duration::from_secs(2) {
return true;
}
}
false
}
pub fn should_upgrade(&self) -> bool {
if self.consecutive_good_frames >= self.thresholds.upgrade_frame_count {
if self.last_quality_change.elapsed() > Duration::from_secs(5) {
return true;
}
}
false
}
pub fn notify_quality_changed(&mut self) {
self.last_quality_change = Instant::now();
self.consecutive_bad_frames = 0;
self.consecutive_good_frames = 0;
}
pub fn average_frame_time(&self) -> Duration {
if self.samples.is_empty() {
return Duration::from_secs(0);
}
let total: Duration = self.samples.iter().map(|s| s.frame_duration).sum();
total / self.samples.len() as u32
}
pub fn current_fps(&self) -> f32 {
let avg = self.average_frame_time();
if avg.as_secs_f32() > 0.0 {
1.0 / avg.as_secs_f32()
} else {
0.0
}
}
pub fn stability(&self) -> f32 {
if self.samples.len() < 10 {
return 1.0;
}
let avg = self.average_frame_time();
let variance: f32 = self
.samples
.iter()
.map(|s| {
let diff = s.frame_duration.as_secs_f32() - avg.as_secs_f32();
diff * diff
})
.sum::<f32>()
/ self.samples.len() as f32;
let std_dev = variance.sqrt();
let stability = 1.0 - (std_dev / avg.as_secs_f32()).min(1.0);
stability.max(0.0)
}
pub fn is_memory_pressure(&self) -> bool {
if let Some(sample) = self.samples.back() {
sample.memory_utilization > self.thresholds.memory_pressure_threshold
} else {
false
}
}
pub fn is_cpu_overloaded(&self) -> bool {
if let Some(sample) = self.samples.back() {
sample.cpu_utilization > self.thresholds.cpu_utilization_threshold
} else {
false
}
}
pub fn stats(&self) -> PerformanceStats {
PerformanceStats {
current_fps: self.current_fps(),
average_frame_time: self.average_frame_time(),
stability: self.stability(),
consecutive_bad_frames: self.consecutive_bad_frames,
consecutive_good_frames: self.consecutive_good_frames,
is_memory_pressure: self.is_memory_pressure(),
is_cpu_overloaded: self.is_cpu_overloaded(),
}
}
pub fn auto_adjust_thresholds(&mut self) {
if self.samples.len() < 60 {
return; }
let avg = self.average_frame_time();
let stability = self.stability();
self.thresholds.adjust_based_on_performance(avg, stability);
}
pub fn thresholds(&self) -> &AdaptivePerformanceThresholds {
&self.thresholds
}
pub fn set_thresholds(&mut self, thresholds: AdaptivePerformanceThresholds) {
self.thresholds = thresholds;
}
}
#[derive(Debug, Clone, Copy)]
pub struct PerformanceStats {
pub current_fps: f32,
pub average_frame_time: Duration,
pub stability: f32,
pub consecutive_bad_frames: usize,
pub consecutive_good_frames: usize,
pub is_memory_pressure: bool,
pub is_cpu_overloaded: bool,
}
pub struct PerformanceTrapDetector {
low_fps_threshold: f32,
sustained_low_fps_frames: usize,
low_fps_counter: usize,
last_warning: Instant,
}
impl PerformanceTrapDetector {
pub fn new(low_fps_threshold: f32, sustained_frames: usize) -> Self {
Self {
low_fps_threshold,
sustained_low_fps_frames: sustained_frames,
low_fps_counter: 0,
last_warning: Instant::now() - Duration::from_secs(300),
}
}
pub fn check(&mut self, fps: f32) -> Option<PerformanceTrap> {
if fps < self.low_fps_threshold {
self.low_fps_counter += 1;
if self.low_fps_counter >= self.sustained_low_fps_frames
&& self.last_warning.elapsed() > Duration::from_secs(30)
{
self.last_warning = Instant::now();
return Some(PerformanceTrap::LowFrameRate {
current_fps: fps,
threshold: self.low_fps_threshold,
});
}
} else {
self.low_fps_counter = 0;
}
None
}
}
#[derive(Debug, Clone)]
pub enum PerformanceTrap {
LowFrameRate { current_fps: f32, threshold: f32 },
MemoryPressure { utilization: f32 },
CpuOverload { utilization: f32 },
BrowserForcedIntegratedGpu,
}
impl PerformanceTrap {
pub fn message(&self) -> String {
match self {
Self::LowFrameRate { current_fps, threshold } => {
format!(
"Performance warning: Frame rate is {:.1} FPS (below {:.1} FPS). Consider lowering graphics quality or closing other applications.",
current_fps, threshold
)
}
Self::MemoryPressure { utilization } => {
format!(
"Memory warning: GPU memory is {:.0}% full. Consider reducing texture quality or closing other applications.",
utilization * 100.0
)
}
Self::CpuOverload { utilization } => {
format!(
"CPU warning: CPU usage is {:.0}%. Software rendering is CPU-intensive. Consider using a GPU if available.",
utilization * 100.0
)
}
Self::BrowserForcedIntegratedGpu => {
"Browser is forcing integrated GPU. For best performance, try running outside browser".to_string()
}
}
}
pub fn suggests_cpu_mode(&self) -> bool {
matches!(self, Self::BrowserForcedIntegratedGpu)
}
pub fn suggests_restart(&self) -> bool {
matches!(self, Self::BrowserForcedIntegratedGpu | Self::CpuOverload { .. })
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_thresholds_for_device_type() {
let discrete = AdaptivePerformanceThresholds::for_device_type(GpuDeviceType::DiscreteGpu);
let integrated =
AdaptivePerformanceThresholds::for_device_type(GpuDeviceType::IntegratedGpu);
let cpu = AdaptivePerformanceThresholds::for_device_type(GpuDeviceType::Cpu);
assert!(discrete.degrade_threshold > integrated.degrade_threshold);
assert!(integrated.degrade_threshold > cpu.degrade_threshold);
assert_eq!(discrete.target_fps, 60.0);
assert_eq!(cpu.target_fps, 30.0);
}
#[test]
fn test_performance_monitor() {
let mut monitor = AdaptivePerformanceMonitor::for_device_type(GpuDeviceType::DiscreteGpu);
monitor.begin_frame();
std::thread::sleep(Duration::from_millis(10));
let sample = monitor.end_frame();
assert_eq!(sample.frame_index, 1);
assert!(sample.frame_duration >= Duration::from_millis(10));
}
#[test]
fn test_trap_detector() {
let mut detector = PerformanceTrapDetector::new(30.0, 5);
for _ in 0..4 {
assert!(detector.check(20.0).is_none());
}
let trap = detector.check(20.0);
assert!(trap.is_some());
if let Some(PerformanceTrap::LowFrameRate { current_fps, .. }) = trap {
assert_eq!(current_fps, 20.0);
}
}
#[test]
fn test_trap_messages() {
let trap = PerformanceTrap::LowFrameRate { current_fps: 15.0, threshold: 30.0 };
let msg = trap.message();
assert!(msg.contains("15.0"));
assert!(msg.contains("30.0"));
}
}