""" Real-time performance graphs for inference latency analysis Shows when latency spikes occur with different resolutions and devices """ from PySide6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QLabel, QGroupBox, QTabWidget, QFrame, QSplitter, QScrollArea ) from PySide6.QtCore import Qt, QTimer, Signal, Slot from PySide6.QtGui import QPainter, QPen, QBrush, QColor, QFont, QLinearGradient import numpy as np from collections import deque from typing import Dict, List, Any import time # Try to import psutil for system monitoring, use fallback if not available try: import psutil PSUTIL_AVAILABLE = True except ImportError: PSUTIL_AVAILABLE = False print("⚠️ psutil not available - system monitoring will use fallback values") class RealTimeGraph(QWidget): """Custom widget for drawing real-time graphs with enhanced styling""" def __init__(self, title: str = "Graph", y_label: str = "Value", max_points: int = 300): super().__init__() self.title = title self.y_label = y_label self.max_points = max_points # Data storage self.x_data = deque(maxlen=max_points) self.y_data = deque(maxlen=max_points) self.spike_markers = deque(maxlen=max_points) # Mark spikes self.device_markers = deque(maxlen=max_points) # Mark device changes self.resolution_markers = deque(maxlen=max_points) # Mark resolution changes # Enhanced styling colors self.bg_color = QColor(18, 18, 18) # Very dark background self.grid_color = QColor(40, 40, 45) # Subtle grid self.line_color = QColor(0, 230, 255) # Bright cyan self.spike_color = QColor(255, 77, 77) # Bright red for spikes self.cpu_color = QColor(120, 180, 255) # Light blue for CPU self.gpu_color = QColor(255, 165, 0) # Orange for GPU self.text_color = QColor(220, 220, 220) # Light gray text self.accent_color = QColor(255, 215, 0) # Gold accent # Auto-scaling self.y_min = 0 self.y_max = 100 self.auto_scale = True # Performance counters self.spike_count = 0 self.device_switches = 0 self.resolution_changes = 0 self.setMinimumSize(400, 200) self.setStyleSheet(""" QWidget { background-color: #121212; border: 1px solid #2a2a2a; border-radius: 8px; } """) def add_data_point(self, x: float, y: float, is_spike: bool = False, device: str = "CPU", is_res_change: bool = False): """Add a new data point to the graph""" self.x_data.append(x) self.y_data.append(y) self.spike_markers.append(is_spike) self.device_markers.append(device) self.resolution_markers.append(is_res_change) # Update counters if is_spike: self.spike_count += 1 if len(self.device_markers) > 1 and device != list(self.device_markers)[-2]: self.device_switches += 1 if is_res_change: self.resolution_changes += 1 # Auto-scale Y axis with better algorithm if self.auto_scale and self.y_data: data_max = max(self.y_data) data_min = min(self.y_data) if data_max > data_min: padding = (data_max - data_min) * 0.15 self.y_max = data_max + padding self.y_min = max(0, data_min - padding * 0.5) else: self.y_max = data_max + 10 if data_max > 0 else 100 self.y_min = 0 self.update() def clear_data(self): """Clear the graph data""" self.x_data.clear() self.y_data.clear() self.spike_markers.clear() self.device_markers.clear() self.resolution_markers.clear() self.spike_count = 0 self.device_switches = 0 self.resolution_changes = 0 self.update() def add_data_point(self, x: float, y: float, is_spike: bool = False, device: str = "CPU", is_res_change: bool = False): """Add a new data point to the graph""" self.x_data.append(x) self.y_data.append(y) self.spike_markers.append(is_spike) self.device_markers.append(device) self.resolution_markers.append(is_res_change) # Auto-scale Y axis if self.auto_scale and self.y_data: data_max = max(self.y_data) data_min = min(self.y_data) padding = (data_max - data_min) * 0.1 self.y_max = data_max + padding if data_max > 0 else 100 self.y_min = max(0, data_min - padding) self.update() def clear_data(self): """Clear the graph data""" self.x_data.clear() self.y_data.clear() self.spike_markers.clear() self.device_markers.clear() self.resolution_markers.clear() self.update() def paintEvent(self, event): """Override paint event to draw the graph with enhanced styling""" painter = QPainter(self) painter.setRenderHint(QPainter.Antialiasing) width = self.width() height = self.height() margin = 50 graph_width = width - 2 * margin graph_height = height - 2 * margin # Enhanced background with subtle gradient gradient = QLinearGradient(0, 0, 0, height) gradient.setColorAt(0, QColor(25, 25, 30)) gradient.setColorAt(1, QColor(15, 15, 20)) painter.fillRect(self.rect(), QBrush(gradient)) # Title with glow effect painter.setPen(self.accent_color) painter.setFont(QFont("Segoe UI", 13, QFont.Bold)) title_rect = painter.fontMetrics().boundingRect(self.title) painter.drawText(15, 25, self.title) # Enhanced axes with better styling painter.setPen(QPen(self.text_color, 2)) painter.drawLine(margin, margin, margin, height - margin) # Y-axis painter.drawLine(margin, height - margin, width - margin, height - margin) # X-axis # Enhanced grid with subtle styling painter.setPen(QPen(self.grid_color, 1, Qt.DotLine)) # Horizontal grid lines for i in range(1, 5): y = margin + (graph_height * i / 4) painter.drawLine(margin + 5, y, width - margin - 5, y) # Vertical grid lines for i in range(1, 10): x = margin + (graph_width * i / 9) painter.drawLine(x, margin + 5, x, height - margin - 5) # Enhanced Y-axis labels with better formatting painter.setPen(self.text_color) painter.setFont(QFont("Segoe UI", 9)) for i in range(5): y_val = self.y_min + (self.y_max - self.y_min) * (4 - i) / 4 y_pos = margin + (graph_height * i / 4) if y_val >= 1000: label = f"{y_val/1000:.1f}k" elif y_val >= 1: label = f"{y_val:.1f}" else: label = f"{y_val:.2f}" painter.drawText(5, y_pos + 4, label) # Enhanced Y-axis label with rotation painter.save() painter.setPen(self.text_color) painter.setFont(QFont("Segoe UI", 10)) painter.translate(20, height // 2) painter.rotate(-90) painter.drawText(-len(self.y_label) * 4, 0, self.y_label) painter.restore() # Enhanced data visualization if len(self.x_data) >= 2 and len(self.y_data) >= 2: points = [] spike_points = [] device_changes = [] res_changes = [] x_min = min(self.x_data) if self.x_data else 0 x_max = max(self.x_data) if self.x_data else 1 x_range = x_max - x_min if x_max > x_min else 1 # Prepare point coordinates for i, (x_val, y_val, is_spike, device, is_res_change) in enumerate(zip( self.x_data, self.y_data, self.spike_markers, self.device_markers, self.resolution_markers )): x_screen = margin + (x_val - x_min) / x_range * graph_width y_screen = height - margin - (y_val - self.y_min) / (self.y_max - self.y_min) * graph_height points.append((x_screen, y_screen)) if is_spike: spike_points.append((x_screen, y_screen)) if i > 0 and device != list(self.device_markers)[i-1]: device_changes.append((x_screen, y_screen, device)) if is_res_change: res_changes.append((x_screen, y_screen)) # Draw main line with enhanced styling if len(points) >= 2: painter.setPen(QPen(self.line_color, 3)) for i in range(len(points) - 1): x1, y1 = points[i] x2, y2 = points[i + 1] painter.drawLine(x1, y1, x2, y2) # Add subtle glow effect to the line painter.setPen(QPen(QColor(self.line_color.red(), self.line_color.green(), self.line_color.blue(), 60), 6)) for i in range(len(points) - 1): x1, y1 = points[i] x2, y2 = points[i + 1] painter.drawLine(x1, y1, x2, y2) # Enhanced spike markers painter.setPen(QPen(self.spike_color, 2)) painter.setBrush(QBrush(self.spike_color)) for x, y in spike_points: painter.drawEllipse(x - 4, y - 4, 8, 8) # Add spike indicator line painter.drawLine(x, y - 10, x, y + 10) # Enhanced device change indicators for x, y, device in device_changes: color = self.gpu_color if device == "GPU" else self.cpu_color painter.setPen(QPen(color, 3)) painter.setBrush(QBrush(QColor(color.red(), color.green(), color.blue(), 100))) painter.drawRect(x - 3, margin, 6, graph_height) # Add device label painter.setPen(color) painter.setFont(QFont("Segoe UI", 8, QFont.Bold)) painter.drawText(x - 10, margin - 5, device) # Enhanced resolution change indicators for x, y in res_changes: painter.setPen(QPen(QColor(255, 193, 7), 2)) # Amber color painter.drawLine(x, margin, x, height - margin) # Add resolution change marker painter.setBrush(QBrush(QColor(255, 193, 7))) painter.drawEllipse(x - 3, margin - 5, 6, 6) class PerformanceGraphsWidget(QWidget): """Enhanced performance graphs widget with real-time data visualization""" # Define signals for better integration performance_data_updated = Signal(dict) spike_detected = Signal(dict) device_switched = Signal(str) def __init__(self): super().__init__() self.setup_ui() # Enhanced timer setup self.update_timer = QTimer() self.update_timer.timeout.connect(self.update_graphs) self.system_timer = QTimer() self.system_timer.timeout.connect(self.update_system_metrics) try: self.update_timer.start(500) # Update graphs every 500ms for smoother animation self.system_timer.start(1000) # Update system metrics every second except Exception as e: print(f"❌ Error starting performance graph timers: {e}") # Enhanced data tracking self.start_time = time.time() if time else None self.latest_data = {} self.cpu_usage_history = deque(maxlen=300) self.ram_usage_history = deque(maxlen=300) # Add missing ram_usage_history self.frame_counter = 0 self.spike_threshold = 100.0 # Default spike threshold in ms self.previous_device = "CPU" # Track device changes # Performance statistics self.latency_stats = { 'avg': 0.0, 'max': 0.0, 'min': float('inf'), 'spike_count': 0 } def __del__(self): """Clean up timers when widget is destroyed""" try: if hasattr(self, 'system_timer') and self.system_timer: self.system_timer.stop() self.system_timer.deleteLater() if hasattr(self, 'update_timer') and self.update_timer: self.update_timer.stop() self.update_timer.deleteLater() except: pass def closeEvent(self, event): """Handle widget close event""" try: if hasattr(self, 'system_timer') and self.system_timer: self.system_timer.stop() if hasattr(self, 'update_timer') and self.update_timer: self.update_timer.stop() except: pass super().closeEvent(event) self.ram_usage_history = deque(maxlen=300) self.spike_threshold = 100 # ms threshold for latency spikes self.previous_device = "CPU" self.frame_counter = 0 # Performance statistics self.latency_stats = { 'avg': 0.0, 'max': 0.0, 'min': float('inf'), 'spike_count': 0 } self.setStyleSheet(""" QWidget { background-color: #121212; color: #ffffff; } QLabel { color: #ffffff; background: transparent; } QFrame { background-color: #1a1a1a; border: 1px solid #333333; border-radius: 8px; margin: 2px; } """) def setup_ui(self): # Create main layout main_layout = QVBoxLayout(self) main_layout.setContentsMargins(5, 5, 5, 5) main_layout.setSpacing(0) # Create scroll area scroll_area = QScrollArea() scroll_area.setWidgetResizable(True) scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) scroll_area.setStyleSheet(""" QScrollArea { border: none; background-color: #121212; } QScrollBar:vertical { background-color: #2C2C2C; width: 12px; border-radius: 6px; } QScrollBar::handle:vertical { background-color: #555555; border-radius: 6px; min-height: 20px; } QScrollBar::handle:vertical:hover { background-color: #777777; } QScrollBar:horizontal { background-color: #2C2C2C; height: 12px; border-radius: 6px; } QScrollBar::handle:horizontal { background-color: #555555; border-radius: 6px; min-width: 20px; } QScrollBar::handle:horizontal:hover { background-color: #777777; } """) # Create scrollable content widget content_widget = QWidget() content_layout = QVBoxLayout(content_widget) content_layout.setContentsMargins(10, 10, 10, 10) content_layout.setSpacing(8) # Enhanced title section title_frame = QFrame() title_layout = QVBoxLayout(title_frame) title_label = QLabel("🔥 Real-Time Inference Performance & Latency Spike Analysis") title_label.setStyleSheet(""" font-size: 18px; font-weight: bold; color: #FFD700; margin: 8px; background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #FFD700, stop:1 #FFA500); -webkit-background-clip: text; -webkit-text-fill-color: transparent; """) title_layout.addWidget(title_label) # Enhanced system stats stats_layout = QHBoxLayout() self.cpu_ram_stats = QLabel("CPU: 0% | RAM: 0%") self.cpu_ram_stats.setStyleSheet(""" color: #00FFFF; font-weight: bold; font-size: 14px; margin: 4px 8px; padding: 4px 8px; background-color: rgba(0, 255, 255, 0.1); border-radius: 4px; """) stats_layout.addWidget(self.cpu_ram_stats) # Add current model display self.current_model_stats = QLabel("Model: Loading...") self.current_model_stats.setStyleSheet(""" color: #FFD700; font-weight: bold; font-size: 14px; margin: 4px 8px; padding: 4px 8px; background-color: rgba(255, 215, 0, 0.1); border-radius: 4px; """) stats_layout.addWidget(self.current_model_stats) title_layout.addLayout(stats_layout) title_frame.setLayout(title_layout) content_layout.addWidget(title_frame) # Enhanced splitter for graphs - set minimum sizes to avoid cluttering splitter = QSplitter(Qt.Vertical) splitter.setStyleSheet(""" QSplitter::handle { background-color: #333333; height: 3px; } QSplitter::handle:hover { background-color: #555555; } """) # Enhanced Latency graph latency_frame = QFrame() latency_frame.setMinimumHeight(250) # Set minimum height to prevent cluttering latency_frame.setStyleSheet(""" QFrame { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 rgba(30, 30, 35, 255), stop:1 rgba(20, 20, 25, 255)); border: 2px solid #00FFFF; border-radius: 10px; } """) latency_layout = QVBoxLayout(latency_frame) self.latency_graph = RealTimeGraph( "Inference Latency Over Time", "Latency (ms)", max_points=300 ) self.latency_graph.setMinimumHeight(200) # Ensure minimum display height latency_layout.addWidget(self.latency_graph) latency_info = QHBoxLayout() self.latency_stats_label = QLabel("Avg: 0ms | Max: 0ms | Spikes: 0") self.latency_stats_label.setStyleSheet(""" color: #00FFFF; font-weight: bold; font-size: 12px; padding: 4px 8px; background-color: rgba(0, 255, 255, 0.15); border-radius: 4px; margin: 4px; """) latency_info.addWidget(self.latency_stats_label) latency_info.addStretch() latency_layout.addLayout(latency_info) latency_frame.setLayout(latency_layout) splitter.addWidget(latency_frame) # Enhanced FPS graph fps_frame = QFrame() fps_frame.setMinimumHeight(250) # Set minimum height to prevent cluttering fps_frame.setStyleSheet(""" QFrame { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 rgba(30, 35, 30, 255), stop:1 rgba(20, 25, 20, 255)); border: 2px solid #00FF00; border-radius: 10px; } """) fps_layout = QVBoxLayout(fps_frame) self.fps_graph = RealTimeGraph( "FPS & Resolution Impact", "FPS", max_points=300 ) self.fps_graph.setMinimumHeight(200) # Ensure minimum display height fps_layout.addWidget(self.fps_graph) fps_info = QHBoxLayout() self.fps_stats = QLabel("Current FPS: 0 | Resolution: - | Device: -") self.fps_stats.setStyleSheet(""" color: #00FF00; font-weight: bold; font-size: 12px; padding: 4px 8px; background-color: rgba(0, 255, 0, 0.15); border-radius: 4px; margin: 4px; """) fps_info.addWidget(self.fps_stats) fps_info.addStretch() fps_layout.addLayout(fps_info) fps_frame.setLayout(fps_layout) splitter.addWidget(fps_frame) # Enhanced Device switching & resolution changes graph device_frame = QFrame() device_frame.setMinimumHeight(220) # Set minimum height to prevent cluttering device_frame.setStyleSheet(""" QFrame { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 rgba(35, 30, 30, 255), stop:1 rgba(25, 20, 20, 255)); border: 2px solid #FFB300; border-radius: 10px; } """) device_layout = QVBoxLayout(device_frame) self.device_graph = RealTimeGraph( "Device Switching & Resolution Changes", "Events", max_points=300 ) self.device_graph.setMinimumHeight(170) # Ensure minimum display height device_layout.addWidget(self.device_graph) self.device_legend = QLabel( " CPU Spikes: 0 | " " GPU Spikes: 0 | " " Switches: 0 | " " Res Changes: 0" ) self.device_legend.setStyleSheet(""" color: #FFB300; font-size: 12px; font-weight: bold; margin: 4px 8px; padding: 4px 8px; background-color: rgba(255, 179, 0, 0.15); border-radius: 4px; """) device_layout.addWidget(self.device_legend) device_frame.setLayout(device_layout) splitter.addWidget(device_frame) # Set splitter proportions with minimum space for each section splitter.setSizes([300, 300, 250]) # Increased minimum sizes splitter.setChildrenCollapsible(False) # Prevent collapsing sections content_layout.addWidget(splitter) content_widget.setLayout(content_layout) # Set minimum size for content widget to ensure scrolling when needed content_widget.setMinimumSize(400, 850) # Minimum width and height # Add content widget to scroll area scroll_area.setWidget(content_widget) # Add scroll area to main layout main_layout.addWidget(scroll_area) self.setLayout(main_layout) fps_layout = QVBoxLayout(fps_frame) self.fps_graph = RealTimeGraph( "FPS & Resolution Impact", "FPS", max_points=300 ) fps_layout.addWidget(self.fps_graph) fps_info = QHBoxLayout() self.fps_stats = QLabel("Current FPS: 0 | Resolution: - | Device: -") self.fps_stats.setStyleSheet("color: #00FF00; font-weight: bold;") fps_info.addWidget(self.fps_stats) fps_info.addStretch() fps_layout.addLayout(fps_info) fps_frame.setLayout(fps_layout) splitter.addWidget(fps_frame) # Device switching & resolution changes graph device_frame = QFrame() device_layout = QVBoxLayout(device_frame) self.device_graph = RealTimeGraph( "Device Switching & Resolution Changes", "-", max_points=300 ) device_layout.addWidget(self.device_graph) # Add scroll area to main layout main_layout.addWidget(scroll_area) self.setLayout(main_layout) @Slot() def update_system_metrics(self): """Update system CPU and RAM usage""" try: # Check if the widget is still valid and not being destroyed if not self or not hasattr(self, 'isVisible') or not self.isVisible(): return # Check if widgets still exist before updating if not hasattr(self, 'cpu_ram_stats') or not self.cpu_ram_stats: return if not hasattr(self, 'device_graph') or not self.device_graph: return # Check if the RealTimeGraph objects are still valid try: if hasattr(self.device_graph, 'add_data_point'): # Test if the object is still valid by accessing a simple property _ = self.device_graph.objectName() else: return except RuntimeError: # Object has been deleted return if PSUTIL_AVAILABLE: cpu_percent = psutil.cpu_percent(interval=None) memory = psutil.virtual_memory() ram_percent = memory.percent else: # Fallback values when psutil is not available cpu_percent = 0.0 ram_percent = 0.0 if hasattr(self, 'cpu_usage_history'): self.cpu_usage_history.append(cpu_percent) if hasattr(self, 'ram_usage_history'): self.ram_usage_history.append(ram_percent) # Update display try: if PSUTIL_AVAILABLE: self.cpu_ram_stats.setText(f"CPU: {cpu_percent:.1f}% | RAM: {ram_percent:.1f}%") else: self.cpu_ram_stats.setText("CPU: -- | RAM: -- (monitoring unavailable)") except RuntimeError: # Widget has been deleted return # Add CPU usage to device graph as background metric try: current_time = time.time() - self.start_time if self.start_time else 0 self.device_graph.add_data_point(current_time, cpu_percent, device="System") except RuntimeError: # Graph has been deleted return except Exception as e: print(f"❌ Error updating system metrics: {e}") # Fallback in case of any error try: if hasattr(self, 'cpu_ram_stats') and self.cpu_ram_stats: self.cpu_ram_stats.setText("CPU: -- | RAM: -- (error)") except: pass @Slot() def update_graphs(self): """Update graphs with latest data""" if not self.latest_data: return try: chart_data = self.latest_data.get('chart_data', {}) latency_stats = self.latest_data.get('latency_stats', {}) current_metrics = self.latest_data.get('current_metrics', {}) if not chart_data.get('timestamps'): return # Get the latest data point timestamps = chart_data.get('timestamps', []) if not timestamps: return latest_timestamp = timestamps[-1] current_time = time.time() - self.start_time if self.start_time else latest_timestamp # Update latency graph if 'inference_latency' in chart_data: latency_values = chart_data['inference_latency'] if latency_values: latest_latency = latency_values[-1] is_spike = latest_latency > self.spike_threshold device = current_metrics.get('device', 'CPU') self.latency_graph.add_data_point( current_time, latest_latency, is_spike=is_spike, device=device ) # Update latency statistics self.latency_stats['max'] = max(self.latency_stats['max'], latest_latency) self.latency_stats['min'] = min(self.latency_stats['min'], latest_latency) if is_spike: self.latency_stats['spike_count'] += 1 # Emit spike signal self.spike_detected.emit({ 'latency': latest_latency, 'timestamp': current_time, 'device': device }) # Calculate running average if hasattr(self.latency_graph, 'y_data') and self.latency_graph.y_data: self.latency_stats['avg'] = sum(self.latency_graph.y_data) / len(self.latency_graph.y_data) # Update FPS graph if 'fps' in chart_data: fps_values = chart_data['fps'] if fps_values: latest_fps = fps_values[-1] device = current_metrics.get('device', 'CPU') resolution = current_metrics.get('resolution', 'Unknown') # Check for device switch device_switched = device != self.previous_device if device_switched: self.device_switched.emit(device) self.previous_device = device self.fps_graph.add_data_point( current_time, latest_fps, device=device, is_res_change=False # Will be set by resolution change detection ) # Update FPS stats display with model name model_name = current_metrics.get('model', 'Unknown') self.fps_stats.setText(f"Current FPS: {latest_fps:.1f} | Resolution: {resolution} | Device: {device} | Model: {model_name}") # Update device switching graph device_usage = chart_data.get('device_usage', []) if device_usage: latest_usage = device_usage[-1] device = current_metrics.get('device', 'CPU') self.device_graph.add_data_point( current_time, latest_usage * 100, # Convert to percentage device=device ) # Update statistics displays self.latency_stats_label.setText( f"Avg: {self.latency_stats['avg']:.1f}ms | " f"Max: {self.latency_stats['max']:.1f}ms | " f"Spikes: {self.latency_stats['spike_count']}" ) # Update device legend self.device_legend.setText( f" CPU Spikes: {self.latency_graph.spike_count} | " f" GPU Spikes: {self.device_graph.spike_count} | " f" Switches: {self.device_graph.device_switches} | " f" Res Changes: {self.device_graph.resolution_changes}" ) # Update current model display model_name = current_metrics.get('model', 'Unknown') device = current_metrics.get('device', 'Unknown') if hasattr(self, 'current_model_stats'): self.current_model_stats.setText(f"Model: {model_name} | Device: {device}") self.frame_counter += 1 except Exception as e: print(f"❌ Error updating performance graphs: {e}") def update_performance_data(self, analytics_data: Dict[str, Any]): """Update graphs with new analytics data, including system metrics""" try: print(f"[PERF DEBUG] update_performance_data called with: {analytics_data}") # Initialize start time if not set if self.start_time is None: self.start_time = time.time() chart_data = analytics_data.get('real_time_data', {}) latency_stats = analytics_data.get('latency_statistics', {}) current_metrics = analytics_data.get('current_metrics', {}) system_metrics = analytics_data.get('system_metrics', {}) if not chart_data.get('timestamps'): print("[PERF DEBUG] No timestamps in chart_data") return self.latest_data = { 'chart_data': chart_data, 'latency_stats': latency_stats, 'current_metrics': current_metrics, 'system_metrics': system_metrics } # Emit signal for other components self.performance_data_updated.emit(analytics_data) # Immediately update graphs on new data self.update_graphs() except Exception as e: print(f"❌ Error updating performance data: {e}") def clear_all_graphs(self): """Clear all graph data""" try: self.latency_graph.clear_data() self.fps_graph.clear_data() self.device_graph.clear_data() # Reset statistics self.latency_stats = { 'avg': 0.0, 'max': 0.0, 'min': float('inf'), 'spike_count': 0 } self.frame_counter = 0 self.start_time = time.time() # Update displays self.latency_stats_label.setText("Avg: 0ms | Max: 0ms | Spikes: 0") self.fps_stats.setText("Current FPS: 0 | Resolution: - | Device: -") self.device_legend.setText( " CPU Spikes: 0 | " " GPU Spikes: 0 | " " Switches: 0 | " " Res Changes: 0" ) except Exception as e: print(f"❌ Error clearing graphs: {e}") def set_spike_threshold(self, threshold: float): """Set the threshold for detecting latency spikes""" self.spike_threshold = threshold def get_performance_summary(self) -> Dict[str, Any]: """Get a summary of current performance metrics""" return { 'latency_stats': self.latency_stats.copy(), 'frame_count': self.frame_counter, 'cpu_usage': list(self.cpu_usage_history), 'ram_usage': list(self.ram_usage_history), 'current_device': self.previous_device }