Skip to content

Commit bf35c21

Browse files
committed
fix(system-monitor): smooth perf plot x-axis scrolling
1 parent c094daf commit bf35c21

1 file changed

Lines changed: 113 additions & 36 deletions

File tree

src/pyqt_reactive/widgets/system_monitor.py

Lines changed: 113 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -586,8 +586,13 @@ def create_pyqtgraph_section(self) -> QWidget:
586586
# Style CPU/GPU plot - minimal padding
587587
self.cpu_gpu_plot.setBackground(self.color_scheme.to_hex(self.color_scheme.panel_bg))
588588
self.cpu_gpu_plot.setYRange(0, 100)
589-
self.cpu_gpu_plot.setXRange(0, self.monitor_config.history_duration_seconds)
589+
# X axis shows "seconds ago", so range is (-history, 0) with 0 = now (right edge)
590+
self.cpu_gpu_plot.setXRange(-self.monitor_config.history_duration_seconds, 0)
590591
self.cpu_gpu_plot.showGrid(x=self.monitor_config.show_grid, y=self.monitor_config.show_grid, alpha=0.3)
592+
# Disable auto-ranging so manual panning works reliably
593+
_cpu_vb = self.cpu_gpu_plot.getPlotItem().getViewBox()
594+
_cpu_vb.setAutoPan(x=False, y=False)
595+
_cpu_vb.disableAutoRange()
591596

592597
# Minimize left axis
593598
self.cpu_gpu_plot.getAxis('left').setTextPen('white')
@@ -605,8 +610,13 @@ def create_pyqtgraph_section(self) -> QWidget:
605610
# Style RAM/VRAM plot - minimal padding
606611
self.ram_vram_plot.setBackground(self.color_scheme.to_hex(self.color_scheme.panel_bg))
607612
self.ram_vram_plot.setYRange(0, 100)
608-
self.ram_vram_plot.setXRange(0, self.monitor_config.history_duration_seconds)
613+
# X axis shows "seconds ago", so range is (-history, 0) with 0 = now (right edge)
614+
self.ram_vram_plot.setXRange(-self.monitor_config.history_duration_seconds, 0)
609615
self.ram_vram_plot.showGrid(x=self.monitor_config.show_grid, y=self.monitor_config.show_grid, alpha=0.3)
616+
# Disable auto-ranging so manual panning works reliably
617+
_ram_vb = self.ram_vram_plot.getPlotItem().getViewBox()
618+
_ram_vb.setAutoPan(x=False, y=False)
619+
_ram_vb.disableAutoRange()
610620

611621
# Minimize left axis
612622
self.ram_vram_plot.getAxis('left').setTextPen('white')
@@ -889,56 +899,123 @@ def _get_interpolated_metrics(self) -> Optional[dict]:
889899
def update_pyqtgraph_plots(self, metrics: Optional[dict] = None):
890900
"""Update consolidated PyQtGraph plots with current data - non-blocking and fast."""
891901
try:
892-
# Convert data point indices to time values in seconds
893902
data_length = len(self.monitor.cpu_history)
894903
if data_length == 0:
895904
return
896905

897-
# Create time axis: each data point represents update_interval_seconds
898-
update_interval = self.monitor_config.update_interval_seconds
899-
x_time = [i * update_interval for i in range(data_length)]
900-
901-
# Get current data
902-
cpu_data = list(self.monitor.cpu_history)
903-
ram_data = list(self.monitor.ram_history)
904-
gpu_data = list(self.monitor.gpu_history)
905-
vram_data = list(self.monitor.vram_history)
906-
907-
# Update CPU/GPU consolidated plot
908-
self.cpu_curve.setData(x_time, cpu_data)
909-
910-
# Handle GPU data (may not be available)
911-
if any(gpu_data):
912-
self.gpu_curve.setData(x_time, gpu_data)
906+
import numpy as _np
907+
908+
update_interval = float(self.monitor_config.update_interval_seconds)
909+
history = float(self.monitor_config.history_duration_seconds)
910+
now = time.time()
911+
912+
# Snapshot histories
913+
cpu_hist = _np.asarray(self.monitor.cpu_history, dtype=_np.float32)
914+
ram_hist = _np.asarray(self.monitor.ram_history, dtype=_np.float32)
915+
gpu_hist = _np.asarray(self.monitor.gpu_history, dtype=_np.float32)
916+
vram_hist = _np.asarray(self.monitor.vram_history, dtype=_np.float32)
917+
ts = _np.asarray(self.monitor.time_stamps, dtype=_np.float64)
918+
919+
if update_interval <= 0:
920+
update_interval = 0.2
921+
922+
# The timestamp deque is prefilled with zeros. Fill those zeros with
923+
# synthetic timestamps spaced by update_interval so the plot is
924+
# visible immediately.
925+
nz = _np.nonzero(ts > 0)[0]
926+
if nz.size:
927+
last_i = int(nz[-1])
928+
# Fill backwards
929+
for i in range(last_i - 1, -1, -1):
930+
if ts[i] <= 0:
931+
ts[i] = ts[i + 1] - update_interval
932+
# Fill forwards (rare, but keep monotonic)
933+
for i in range(last_i + 1, data_length):
934+
if ts[i] <= 0:
935+
ts[i] = ts[i - 1] + update_interval
913936
else:
914-
self.gpu_curve.setData([], []) # Clear data
937+
idx = _np.arange(data_length, dtype=_np.float64)
938+
ts = now - (data_length - 1 - idx) * update_interval
939+
940+
# Convert to relative x in seconds (negative = older; 0 = now)
941+
base_x = ts - now
942+
943+
# Densify tail from newest sample -> now so x scrolls continuously
944+
try:
945+
rf = float(getattr(self.monitor_config, "render_fps", 60.0))
946+
uf = float(getattr(self.monitor_config, "update_fps", 5.0))
947+
subdivisions = max(4, int(min(64, round(rf / max(1.0, uf)))))
948+
except Exception:
949+
subdivisions = 8
950+
951+
def _series(hist: _np.ndarray, key: str) -> tuple[_np.ndarray, _np.ndarray]:
952+
if data_length == 1:
953+
x0 = float(base_x[-1])
954+
y0 = float(hist[-1])
955+
y1 = float(metrics.get(key, y0)) if metrics is not None else y0
956+
x_tail = _np.linspace(x0, 0.0, subdivisions, dtype=_np.float64)
957+
y_tail = _np.linspace(y0, y1, subdivisions, dtype=_np.float32)
958+
return x_tail, y_tail
959+
960+
x0 = float(base_x[-1])
961+
y0 = float(hist[-1])
962+
y1 = float(metrics.get(key, y0)) if metrics is not None else y0
963+
x_tail = _np.linspace(x0, 0.0, subdivisions, dtype=_np.float64)
964+
y_tail = _np.linspace(y0, y1, subdivisions, dtype=_np.float32)
965+
x_full = _np.concatenate((base_x[:-1], x_tail))
966+
y_full = _np.concatenate((hist[:-1], y_tail))
967+
return x_full, y_full
968+
969+
x_cpu, y_cpu = _series(cpu_hist, "cpu_percent")
970+
_, y_gpu = _series(gpu_hist, "gpu_percent")
971+
_, y_ram = _series(ram_hist, "ram_percent")
972+
_, y_vram = _series(vram_hist, "vram_percent")
973+
974+
# Keep x range fixed to the last `history` seconds.
975+
try:
976+
self.cpu_gpu_plot.setXRange(-history, 0.0, padding=0)
977+
self.ram_vram_plot.setXRange(-history, 0.0, padding=0)
978+
except Exception:
979+
pass
980+
981+
self.cpu_curve.setData(x_cpu, y_cpu)
982+
if _np.any(y_gpu):
983+
self.gpu_curve.setData(x_cpu, y_gpu)
984+
else:
985+
self.gpu_curve.setData([], [])
915986

916987
# Update CPU/GPU plot title with current values
917-
if metrics:
918-
cpu_status = f"{metrics.get('cpu_percent', 0.0):.1f}%"
919-
gpu_status = f"{metrics.get('gpu_percent', 0.0):.1f}%" if any(gpu_data) else "Not Available"
988+
if metrics is not None:
989+
cpu_status = f"{float(metrics.get('cpu_percent', 0.0)):.1f}%"
990+
if _np.any(y_gpu):
991+
gpu_status = f"{float(metrics.get('gpu_percent', 0.0)):.1f}%"
992+
else:
993+
gpu_status = "Not Available"
920994
else:
921-
cpu_status = f'{cpu_data[-1]:.1f}%' if cpu_data else 'N/A'
922-
gpu_status = f'{gpu_data[-1]:.1f}%' if gpu_data else 'Not Available'
923-
self.cpu_gpu_plot.setTitle(f'CPU: {cpu_status}, GPU: {gpu_status}')
995+
cpu_status = f"{float(y_cpu[-1]) if y_cpu.size else 0.0:.1f}%" if y_cpu.size else "N/A"
996+
gpu_status = f"{float(y_gpu[-1]) if y_gpu.size else 0.0:.1f}%" if _np.any(y_gpu) else "Not Available"
997+
self.cpu_gpu_plot.setTitle(f"CPU: {cpu_status}, GPU: {gpu_status}")
924998

925-
# Update RAM/VRAM consolidated plot
926-
self.ram_curve.setData(x_time, ram_data)
999+
# Update RAM/VRAM consolidated plot using densified arrays
1000+
self.ram_curve.setData(x_cpu, y_ram)
9271001

9281002
# Handle VRAM data (may not be available)
929-
if any(vram_data):
930-
self.vram_curve.setData(x_time, vram_data)
1003+
if _np.any(y_vram):
1004+
self.vram_curve.setData(x_cpu, y_vram)
9311005
else:
9321006
self.vram_curve.setData([], []) # Clear data
9331007

9341008
# Update RAM/VRAM plot title with current values
935-
if metrics:
936-
ram_status = f"{metrics.get('ram_percent', 0.0):.1f}%"
937-
vram_status = f"{metrics.get('vram_percent', 0.0):.1f}%" if any(vram_data) else "Not Available"
1009+
if metrics is not None:
1010+
ram_status = f"{float(metrics.get('ram_percent', 0.0)):.1f}%"
1011+
if _np.any(y_vram):
1012+
vram_status = f"{float(metrics.get('vram_percent', 0.0)):.1f}%"
1013+
else:
1014+
vram_status = "Not Available"
9381015
else:
939-
ram_status = f'{ram_data[-1]:.1f}%' if ram_data else 'N/A'
940-
vram_status = f'{vram_data[-1]:.1f}%' if vram_data else 'Not Available'
941-
self.ram_vram_plot.setTitle(f'RAM: {ram_status}, VRAM: {vram_status}')
1016+
ram_status = f"{float(y_ram[-1]) if y_ram.size else 0.0:.1f}%" if y_ram.size else "N/A"
1017+
vram_status = f"{float(y_vram[-1]) if y_vram.size else 0.0:.1f}%" if _np.any(y_vram) else "Not Available"
1018+
self.ram_vram_plot.setTitle(f"RAM: {ram_status}, VRAM: {vram_status}")
9421019

9431020
except Exception as e:
9441021
logger.warning(f"Failed to update PyQtGraph plots: {e}")

0 commit comments

Comments
 (0)