Skip to content

Commit e5fcd84

Browse files
committed
improve metrics bar functionality
1 parent 651c74d commit e5fcd84

File tree

5 files changed

+247
-321
lines changed

5 files changed

+247
-321
lines changed

core/monitoring/collectors.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,21 @@
1-
"""Metrics collection threads."""
1+
from threading import Event
22
from PySide6.QtCore import QThread, Signal
33

4-
from core.monitoring.system_metrics import SystemMetrics, SystemMonitor
4+
from core.monitoring.system_metrics import SystemMonitor
5+
56

67
class MetricsCollector(QThread):
7-
"""Background thread for collecting system metrics."""
8-
9-
metrics_updated = Signal(object) # SystemMetrics
10-
11-
def __init__(self, interval: int = 200):
8+
9+
metrics_updated = Signal(object)
10+
11+
def __init__(self, interval: int = 400):
1212
super().__init__()
1313
self.interval = interval
14-
self.running = True
14+
self._stop_event = Event()
1515
self.monitor = SystemMonitor()
1616

1717
def run(self):
18-
"""Collect metrics at regular intervals."""
19-
while self.running:
18+
while not self._stop_event.is_set():
2019
try:
2120
metrics = self.monitor.collect_all_metrics()
2221
self.metrics_updated.emit(metrics)
@@ -26,5 +25,7 @@ def run(self):
2625
self.msleep(self.interval)
2726

2827
def stop(self):
29-
"""Stop metrics collection."""
30-
self.running = False
28+
self._stop_event.set()
29+
30+
def cleanup(self):
31+
self.monitor.shutdown()

core/monitoring/metrics_store.py

Lines changed: 13 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,32 @@
1-
"""Metrics data storage and subscription management."""
21
import threading
3-
from typing import List, Callable
2+
from typing import List, Optional
3+
4+
from PySide6.QtCore import QObject, Signal
45

56
from core.monitoring.system_metrics import SystemMetrics
67

7-
class MetricsStore:
8-
"""Store and distribute system metrics to subscribers."""
9-
10-
def __init__(self, buffer_size: int = 100):
8+
9+
class MetricsStore(QObject):
10+
11+
metrics_ready = Signal(object)
12+
13+
def __init__(self, buffer_size: int = 100, parent=None):
14+
super().__init__(parent)
1115
self.buffer_size = buffer_size
1216
self.metrics_history: List[SystemMetrics] = []
13-
self._subscribers: List[Callable[[SystemMetrics], None]] = []
1417
self._lock = threading.Lock()
1518

1619
def add_metrics(self, metrics: SystemMetrics) -> None:
17-
"""Add new metrics and notify subscribers."""
1820
with self._lock:
1921
self.metrics_history.append(metrics)
2022
if len(self.metrics_history) > self.buffer_size:
2123
self.metrics_history.pop(0)
22-
self._notify_subscribers(metrics)
23-
24-
def subscribe(self, callback: Callable[[SystemMetrics], None]) -> None:
25-
"""Subscribe to metrics updates."""
26-
with self._lock:
27-
self._subscribers.append(callback)
28-
29-
def unsubscribe(self, callback: Callable[[SystemMetrics], None]) -> None:
30-
"""Unsubscribe from metrics updates."""
31-
with self._lock:
32-
if callback in self._subscribers:
33-
self._subscribers.remove(callback)
24+
self.metrics_ready.emit(metrics)
3425

35-
def _notify_subscribers(self, metrics: SystemMetrics) -> None:
36-
"""Notify all subscribers of new metrics."""
37-
with self._lock:
38-
subscribers = self._subscribers.copy()
39-
40-
for subscriber in subscribers:
41-
try:
42-
subscriber(metrics)
43-
except Exception as e:
44-
print(f"Error notifying subscriber: {e}")
45-
46-
def get_latest_metrics(self) -> SystemMetrics:
47-
"""Get the most recent metrics."""
26+
def get_latest_metrics(self) -> Optional[SystemMetrics]:
4827
with self._lock:
4928
return self.metrics_history[-1] if self.metrics_history else None
50-
29+
5130
def clear(self) -> None:
52-
"""Clear all stored metrics."""
5331
with self._lock:
5432
self.metrics_history.clear()

core/monitoring/system_metrics.py

Lines changed: 29 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,28 @@
1-
"""System metrics data structures and monitoring utilities."""
21
from dataclasses import dataclass
32
from datetime import datetime
43
from typing import Optional
54

65
import psutil
76
import torch
87

8+
99
@dataclass
1010
class SystemMetrics:
11-
"""Container for system metrics data."""
1211
timestamp: datetime
1312
cpu_usage: float
1413
ram_usage_percent: float
1514
gpu_utilization: Optional[float] = None
1615
vram_usage_percent: Optional[float] = None
1716
power_usage_percent: Optional[float] = None
18-
power_limit_percent: Optional[float] = None
17+
power_limit_watts: Optional[float] = None
1918

2019

2120
class SystemMonitor:
22-
"""System resource monitoring utilities."""
23-
21+
2422
def __init__(self):
2523
self.has_nvidia = self._init_nvidia()
26-
24+
2725
def _init_nvidia(self) -> bool:
28-
"""Initialize NVIDIA monitoring if available."""
2926
try:
3027
if torch.cuda.is_available() and "nvidia" in torch.cuda.get_device_name(0).lower():
3128
import pynvml
@@ -34,44 +31,39 @@ def _init_nvidia(self) -> bool:
3431
return True
3532
except Exception:
3633
pass
37-
34+
3835
self.gpu_handle = None
3936
return False
40-
37+
4138
def is_nvidia_gpu_available(self) -> bool:
42-
"""Check if NVIDIA GPU is available."""
4339
return self.has_nvidia
44-
40+
4541
def collect_cpu_metrics(self) -> float:
46-
"""Collect CPU usage metrics."""
4742
percentages = psutil.cpu_percent(interval=0, percpu=True)
4843
return sum(percentages) / len(percentages) if percentages else 0
49-
44+
5045
def collect_ram_metrics(self) -> tuple[float, int]:
51-
"""Collect RAM usage metrics."""
5246
ram = psutil.virtual_memory()
5347
return ram.percent, ram.used
54-
48+
5549
def collect_gpu_metrics(self) -> tuple[float, float, float, float]:
56-
"""Collect GPU metrics if available."""
5750
if not self.gpu_handle:
5851
return 0, 0, 0, 0
59-
52+
6053
try:
6154
import pynvml
6255
memory_info = pynvml.nvmlDeviceGetMemoryInfo(self.gpu_handle)
6356
gpu_utilization = pynvml.nvmlDeviceGetUtilizationRates(self.gpu_handle).gpu
6457
vram_usage_percent = (memory_info.used / memory_info.total) * 100 if memory_info.total > 0 else 0
65-
power_usage_percent, power_limit_percent = self._collect_power_metrics()
66-
return gpu_utilization, vram_usage_percent, power_usage_percent, power_limit_percent
58+
power_usage_percent, power_limit_watts = self._collect_power_metrics()
59+
return gpu_utilization, vram_usage_percent, power_usage_percent, power_limit_watts
6760
except Exception:
6861
return 0, 0, 0, 0
69-
62+
7063
def _collect_power_metrics(self) -> tuple[float, float]:
71-
"""Collect GPU power metrics."""
7264
if not self.gpu_handle:
7365
return 0, 0
74-
66+
7567
try:
7668
import pynvml
7769
power_usage = pynvml.nvmlDeviceGetPowerUsage(self.gpu_handle) / 1000.0
@@ -80,23 +72,32 @@ def _collect_power_metrics(self) -> tuple[float, float]:
8072
return power_percentage, power_limit
8173
except Exception:
8274
return 0, 0
83-
75+
8476
def collect_all_metrics(self) -> SystemMetrics:
85-
"""Collect all system metrics."""
8677
cpu_usage = self.collect_cpu_metrics()
8778
ram_usage_percent, _ = self.collect_ram_metrics()
88-
79+
8980
if self.has_nvidia:
9081
gpu_util, vram_percent, power_percent, power_limit = self.collect_gpu_metrics()
9182
else:
9283
gpu_util = vram_percent = power_percent = power_limit = None
93-
84+
9485
return SystemMetrics(
9586
timestamp=datetime.now(),
9687
cpu_usage=cpu_usage,
9788
ram_usage_percent=ram_usage_percent,
9889
gpu_utilization=gpu_util,
9990
vram_usage_percent=vram_percent,
10091
power_usage_percent=power_percent,
101-
power_limit_percent=power_limit
102-
)
92+
power_limit_watts=power_limit
93+
)
94+
95+
def shutdown(self):
96+
if self.has_nvidia:
97+
try:
98+
import pynvml
99+
pynvml.nvmlShutdown()
100+
except Exception:
101+
pass
102+
self.gpu_handle = None
103+
self.has_nvidia = False

gui/widgets/metrics_bar.py

Lines changed: 35 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,76 @@
1-
"""Main metrics bar widget."""
2-
from PySide6.QtCore import Qt
31
from PySide6.QtWidgets import QWidget, QVBoxLayout, QMenu
4-
from PySide6.QtGui import QPixmapCache
52

63
from core.monitoring.collectors import MetricsCollector
74
from core.monitoring.metrics_store import MetricsStore
5+
from core.monitoring.system_metrics import SystemMonitor
86
from gui.widgets.visualizations import (
97
BarVisualization, SparklineVisualization,
108
SpeedometerVisualization, ArcGraphVisualization
119
)
1210

11+
1312
class MetricsBar(QWidget):
14-
"""Main metrics display widget with multiple visualization options."""
15-
13+
1614
def __init__(self, parent=None):
1715
super().__init__(parent)
18-
QPixmapCache.setCacheLimit(10 * 1024)
1916
self.setToolTip("Right click for display options")
20-
17+
18+
monitor = SystemMonitor()
19+
self._has_nvidia_gpu = monitor.is_nvidia_gpu_available()
20+
monitor.shutdown()
21+
2122
self.metrics_store = MetricsStore(buffer_size=100)
22-
self.current_visualization_type = 1 # Default to sparkline
23-
23+
self.current_visualization_type = 1
24+
2425
self._init_ui()
2526
self._create_visualization(self.current_visualization_type)
2627
self._start_metrics_collector()
27-
28+
2829
def _init_ui(self):
29-
"""Initialize UI layout."""
3030
self.layout = QVBoxLayout(self)
3131
self.layout.setContentsMargins(0, 0, 0, 0)
32-
32+
3333
def _create_visualization(self, viz_type: int):
34-
"""Create visualization widget."""
3534
if hasattr(self, 'current_visualization'):
3635
self.current_visualization.cleanup()
3736
self.layout.removeWidget(self.current_visualization)
3837
self.current_visualization.deleteLater()
39-
38+
4039
visualizations = [
4140
BarVisualization,
4241
SparklineVisualization,
4342
SpeedometerVisualization,
4443
ArcGraphVisualization
4544
]
46-
47-
self.current_visualization = visualizations[viz_type](self.metrics_store)
45+
46+
self.current_visualization = visualizations[viz_type](
47+
self.metrics_store, self._has_nvidia_gpu
48+
)
4849
self.current_visualization.setToolTip("Right click for display options")
4950
self.layout.addWidget(self.current_visualization)
50-
51+
5152
def contextMenuEvent(self, event):
52-
"""Show context menu for visualization options."""
5353
menu = QMenu(self)
54-
55-
# Visualization submenu
54+
5655
visual_menu = menu.addMenu("Visualization")
57-
56+
5857
viz_actions = [
5958
visual_menu.addAction("Bar"),
6059
visual_menu.addAction("Sparkline"),
6160
visual_menu.addAction("Speedometer"),
6261
visual_menu.addAction("Arc")
6362
]
64-
65-
# Mark current visualization
63+
6664
viz_actions[self.current_visualization_type].setCheckable(True)
6765
viz_actions[self.current_visualization_type].setChecked(True)
68-
66+
6967
menu.addSeparator()
70-
71-
# Monitoring control
68+
7269
is_running = hasattr(self, 'metrics_collector') and self.metrics_collector.isRunning()
7370
control_action = menu.addAction("Stop Monitoring" if is_running else "Start Monitoring")
74-
75-
# Execute menu
71+
7672
action = menu.exec_(event.globalPos())
77-
78-
# Handle action
73+
7974
if action in viz_actions:
8075
new_type = viz_actions.index(action)
8176
if new_type != self.current_visualization_type:
@@ -86,29 +81,26 @@ def contextMenuEvent(self, event):
8681
self._stop_metrics_collector()
8782
else:
8883
self._start_metrics_collector()
89-
84+
9085
def _start_metrics_collector(self):
91-
"""Start metrics collection."""
86+
if hasattr(self, 'metrics_collector'):
87+
self._stop_metrics_collector()
88+
9289
self.metrics_collector = MetricsCollector()
9390
self.metrics_collector.metrics_updated.connect(
9491
lambda metrics: self.metrics_store.add_metrics(metrics)
9592
)
9693
self.metrics_collector.start()
97-
94+
9895
def _stop_metrics_collector(self):
99-
"""Stop metrics collection."""
10096
if hasattr(self, 'metrics_collector'):
10197
self.metrics_collector.stop()
10298
self.metrics_collector.wait()
103-
99+
self.metrics_collector.cleanup()
100+
self.metrics_collector.deleteLater()
101+
del self.metrics_collector
102+
104103
def cleanup(self):
105-
"""Clean up resources."""
106104
self._stop_metrics_collector()
107105
if hasattr(self, 'current_visualization'):
108-
self.current_visualization.cleanup()
109-
QPixmapCache.clear()
110-
111-
def closeEvent(self, event):
112-
"""Handle close event."""
113-
self.cleanup()
114-
super().closeEvent(event)
106+
self.current_visualization.cleanup()

0 commit comments

Comments
 (0)