Persistent System Monitor

Background thread-based system monitoring for non-blocking metric collection.

Module: pyqt_reactive.services.persistent_system_monitor

Overview

PersistentSystemMonitor wraps SystemMonitorCore with background thread management. It runs metric collection in a separate thread, preventing the main thread from blocking during system queries.

This is useful for GUI applications (PyQt6, Tkinter, etc.) where blocking the main thread causes UI freezes.

Architecture

Thread-Based Architecture

┌─────────────────────────────────────────────────────────┐
│          PersistentSystemMonitor (Main Thread)        │
│  ┌──────────────────────────────────────────────┐   │
│  │      SystemMonitorCore (Framework-Agnostic)│   │
│  │  - CPU/RAM metrics                         │   │
│  │  - GPU/VRAM metrics (if available)            │   │
│  └──────────────────────────────────────────────┘   │
│                                                     │
│  Background Thread:                                  │
│  ┌──────────────────────────────────────────────┐   │
│  │  Collection Loop (runs every N seconds)     │   │
│  │  1. Collect metrics                       │   │
│  │  2. Store in history                      │   │
│  │  3. Emit signal (if configured)          │   │
│  │  4. Sleep for interval                     │   │
│  │  5. Repeat                                │   │
│  └──────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────┘

Usage

Basic Usage

from pyqt_reactive.services.persistent_system_monitor import PersistentSystemMonitor

# Create persistent monitor
monitor = PersistentSystemMonitor(
    update_interval=2.0,       # Update every 2 seconds
    history_length=300,         # Keep 300 points (5 minutes)
)

# Start background thread
monitor.start()

# Get current metrics (thread-safe)
metrics = monitor.get_current_metrics()
print(f"CPU: {metrics['cpu_percent']}%")

# Stop background thread
monitor.stop()

Update Interval

Time between metric collection updates:

# Default: 2.0 seconds
monitor = PersistentSystemMonitor(update_interval=2.0)

# Fast updates (0.5 seconds)
monitor = PersistentSystemMonitor(update_interval=0.5)

# Slow updates (5.0 seconds)
monitor = PersistentSystemMonitor(update_interval=5.0)

History Length

Number of data points to keep in rolling history:

# Default: 300 points
monitor = PersistentSystemMonitor(history_length=300)

# Longer history (600 points = 10 minutes at 1-second interval)
monitor = PersistentSystemMonitor(history_length=600)

Thread Safety

Thread-Safe Access

PersistentSystemMonitor provides thread-safe access to metrics:

# These methods are thread-safe
metrics = monitor.get_current_metrics()
cpu_history = monitor.get_cpu_history()
ram_history = monitor.get_ram_history()

# Safe to call from any thread
def background_task():
    while True:
        metrics = monitor.get_current_metrics()  # Thread-safe
        # ... use metrics

Thread-Safe Updates

Internal updates are also thread-safe:

# Background thread updates metrics
# Internal locking ensures no race conditions
# No external locking needed

Signals

metrics_updated (pyqtSignal)

Emitted when new metrics are collected. Only available in PyQt6 environment.

monitor.metrics_updated.connect(self.on_metrics_updated)

def on_metrics_updated(self, metrics):
    cpu = metrics.get('cpu_percent')
    ram = metrics.get('ram_percent')
    # ... update UI

Note: This signal is not available in non-PyQt6 environments (CLI, Textual TUI).

Lifecycle Management

Start/Stop

Start and stop the background thread:

# Start background thread
monitor.start()

# Check if running
is_running = monitor.is_running()

# Stop background thread
monitor.stop()

# Wait for thread to finish (optional)
monitor.join()

Restart

Restart the monitor with new configuration:

# Change update interval
monitor.restart(update_interval=1.0)

# Change history length
monitor.restart(history_length=600)

Auto-Start

The monitor does not auto-start. You must call start() explicitly:

monitor = PersistentSystemMonitor(update_interval=2.0)
monitor.start()  # Must call this

Performance Considerations

CPU Overhead

System monitoring has minimal CPU overhead:

  • psutil: ~0.1-0.5% CPU per query

  • GPUtil: ~0.05-0.1% CPU per query (if GPU available)

  • Total: ~0.15-0.6% CPU at 2-second interval

Memory Overhead

Memory usage depends on history length:

# Memory per metric: ~24 bytes (float) + overhead
# 4 metrics * 300 points = ~28.8 KB
# 4 metrics * 600 points = ~57.6 KB

Thread Contention

The background thread sleeps for update_interval seconds, minimizing contention:

# Collection loop (simplified)
while True:
    metrics = collect_metrics()  # Fast (~10-20ms)
    store_metrics(metrics)      # Fast (~1-2ms)
    emit_signal(metrics)       # Fast (~1-2ms if configured)
    time.sleep(update_interval)  # Sleep (2.0 seconds)

Integration with PyQt6

Signal-Based Updates

In PyQt6, use the metrics_updated signal:

from PyQt6.QtWidgets import QWidget
from pyqt_reactive.services.persistent_system_monitor import PersistentSystemMonitor

class MyWidget(QWidget):
    def __init__(self):
        super().__init__()

        # Create monitor
        self.monitor = PersistentSystemMonitor(update_interval=2.0)

        # Connect to signal
        self.monitor.metrics_updated.connect(self.on_metrics_updated)

        # Start
        self.monitor.start()

    def on_metrics_updated(self, metrics):
        # Update UI (runs in main thread)
        cpu = metrics.get('cpu_percent')
        self.cpu_label.setText(f"CPU: {cpu}%")

Integration with Textual TUI

Callback-Based Updates

In Textual TUI, poll the monitor periodically:

from textual.app import App, Compose
from pyqt_reactive.services.persistent_system_monitor import PersistentSystemMonitor

class MonitorApp(App):
    def __init__(self):
        super().__init__()
        self.monitor = PersistentSystemMonitor(update_interval=2.0)
        self.monitor.start()

    def on_timer(self):
        # Poll metrics (no signals in Textual)
        metrics = self.monitor.get_current_metrics()
        self.update_display(metrics)

Integration with SystemMonitorCore

PersistentSystemMonitor wraps SystemMonitorCore:

PersistentSystemMonitor
     │
     ├──> wraps ──> SystemMonitorCore (framework-agnostic)
     │                        │
     │                        ├──> CPU/RAM metrics (psutil)
     │                        └──> GPU/VRAM metrics (GPUtil)
     │
     └──> adds ──> Background thread management

You can access the underlying SystemMonitorCore:

# Access underlying monitor core
core = monitor.core

# Call core methods directly
metrics = core.collect_metrics()

See Also