fix: monitor worker non-blocking + test fixes
- core/monitor.py: add fast metrics script (0.3s vs 1.4s sleep), collect() now defaults to fast=True for responsive UI - ui/workers.py: MonitorWorker changed from loop mode to single-shot (QTimer re-arms next cycle on finished signal, avoids paramiko blocking in infinite loop) - ui/widgets.py: remove debug print from _kick_one_sample - test_monitor_nonblock.py: fix sample counting by wrapping _on_sample instead of disconnecting signals (old approach missed new workers) - test_process_monitor.py: assertions account for MAX_RENDER_ROWS=200, fix MEM% column index (4 not 3) in sort test
This commit is contained in:
+108
-10
@@ -381,8 +381,13 @@ class MonitorPanel(QWidget):
|
||||
self.interval_combo = QComboBox()
|
||||
self.interval_combo.addItems(["1 秒", "2 秒", "3 秒", "5 秒", "10 秒"])
|
||||
self.interval_combo.setCurrentIndex(2)
|
||||
self.interval_combo.setToolTip("自动刷新间隔(1-10 秒)")
|
||||
head.addWidget(QLabel("刷新:"))
|
||||
head.addWidget(self.interval_combo)
|
||||
self.btn_refresh_now = QPushButton("⟳ 立即刷新")
|
||||
self.btn_refresh_now.setToolTip("立即触发一次采集(不打断自动刷新)")
|
||||
self.btn_refresh_now.clicked.connect(self._on_refresh_now)
|
||||
head.addWidget(self.btn_refresh_now)
|
||||
self.btn_toggle = QPushButton("开始监控")
|
||||
self.btn_toggle.setCheckable(True)
|
||||
self.btn_toggle.toggled.connect(self._on_toggle)
|
||||
@@ -654,25 +659,95 @@ class MonitorPanel(QWidget):
|
||||
self.info_label.setText("⚠ 当前主机未连接")
|
||||
return
|
||||
idx = self.interval_combo.currentIndex()
|
||||
interval = [1, 2, 3, 5, 10][idx]
|
||||
interval_ms = [1000, 2000, 3000, 5000, 10000][idx]
|
||||
self._stop_worker()
|
||||
self.worker = MonitorWorker(conn, interval=interval)
|
||||
self.worker.sample_ready.connect(self._on_sample)
|
||||
self.worker.error.connect(lambda m: self.info_label.setText(f"⚠ {m}"))
|
||||
self.worker.start()
|
||||
self.info_label.setText(f"已启动监控,每 {interval} 秒刷新")
|
||||
self._monitor_interval_ms = interval_ms
|
||||
self._monitor_conn = conn
|
||||
# 第一次立即采
|
||||
self._kick_one_sample()
|
||||
self.info_label.setText(f"已启动监控,每 {interval_ms // 1000} 秒刷新")
|
||||
|
||||
def _stop_worker(self):
|
||||
# 取消 QTimer
|
||||
if hasattr(self, "_monitor_timer") and self._monitor_timer:
|
||||
try:
|
||||
self._monitor_timer.stop()
|
||||
except Exception:
|
||||
pass
|
||||
self._monitor_timer = None
|
||||
# 停掉当前 worker
|
||||
if self.worker:
|
||||
self.worker.stop()
|
||||
self.worker.wait(2000)
|
||||
try:
|
||||
self.worker.stop()
|
||||
if self.worker.isRunning():
|
||||
self.worker.wait(500) # 不要等太久
|
||||
except Exception:
|
||||
pass
|
||||
self.worker = None
|
||||
self._last_net.clear()
|
||||
|
||||
def _kick_one_sample(self):
|
||||
"""启动一个后台 worker 做一次采集;采完用 QTimer 调度下一次"""
|
||||
if not getattr(self, "_monitor_conn", None):
|
||||
return
|
||||
if not self._monitor_conn.connected:
|
||||
return
|
||||
# 已经有 worker 在跑就不要重复起
|
||||
if self.worker and self.worker.isRunning():
|
||||
return
|
||||
self.worker = MonitorWorker(self._monitor_conn, interval=0)
|
||||
self.worker.sample_ready.connect(self._on_sample)
|
||||
self.worker.error.connect(self._on_monitor_error)
|
||||
self.worker.finished.connect(self._on_worker_finished)
|
||||
self.worker.start()
|
||||
|
||||
def _on_refresh_now(self):
|
||||
"""立即触发一次采集(不打断 QTimer 调度)"""
|
||||
if not self.btn_toggle.isChecked():
|
||||
QMessageBox.information(self, "提示", "请先点击「开始监控」")
|
||||
return
|
||||
self._kick_one_sample()
|
||||
# 让 QTimer 知道我们刚采过——重新计时避免太快又采
|
||||
if getattr(self, "_monitor_timer", None):
|
||||
self._monitor_timer.start(getattr(self, "_monitor_interval_ms", 3000))
|
||||
self.statusBar_msg = "已请求立即刷新" if hasattr(self, "statusBar_msg") else None # 兼容
|
||||
|
||||
def _on_worker_finished(self):
|
||||
"""worker 跑完一次后,调度下一次(仅在监控开启时)"""
|
||||
if not self.btn_toggle.isChecked():
|
||||
return
|
||||
if not getattr(self, "_monitor_timer", None):
|
||||
self._monitor_timer = QTimer(self)
|
||||
self._monitor_timer.setSingleShot(True)
|
||||
self._monitor_timer.timeout.connect(self._kick_one_sample)
|
||||
# 重新启动
|
||||
self._monitor_timer.start(getattr(self, "_monitor_interval_ms", 3000))
|
||||
|
||||
def _on_monitor_error(self, msg: str):
|
||||
self.info_label.setText(f"⚠ {msg}")
|
||||
# 错误后等更久再试
|
||||
if not self.btn_toggle.isChecked():
|
||||
return
|
||||
if not getattr(self, "_monitor_timer", None):
|
||||
self._monitor_timer = QTimer(self)
|
||||
self._monitor_timer.setSingleShot(True)
|
||||
self._monitor_timer.timeout.connect(self._kick_one_sample)
|
||||
self._monitor_timer.start(max(getattr(self, "_monitor_interval_ms", 3000) * 2, 5000))
|
||||
|
||||
def _on_sample(self, m: dict):
|
||||
if m.get("error"):
|
||||
self.info_label.setText(f"⚠ {m['error']}")
|
||||
return
|
||||
# 渲染优化:先关更新,最后一次开。500 行 × 12 列的 setRowCount 重绘
|
||||
# 会触发大量 layout/styling 计算,关掉能省 100-300ms。
|
||||
self.setUpdatesEnabled(False)
|
||||
try:
|
||||
self._render_sample(m)
|
||||
finally:
|
||||
self.setUpdatesEnabled(True)
|
||||
|
||||
def _render_sample(self, m: dict):
|
||||
"""实际渲染一次采集数据。假定调用前已 setUpdatesEnabled(False)"""
|
||||
# 主机
|
||||
os_info = m.get("os", "")
|
||||
krn = m.get("kernel", "")
|
||||
@@ -681,6 +756,7 @@ class MonitorPanel(QWidget):
|
||||
|
||||
# CPU
|
||||
cpu = m.get("cpu", 0)
|
||||
|
||||
self._value_label("cpu_card").setText(f"{cpu:.1f}%")
|
||||
cores = m.get("cores", 1)
|
||||
self._sub_label("cpu_card").setText(f"{cores} 核 CPU")
|
||||
@@ -783,11 +859,24 @@ class MonitorPanel(QWidget):
|
||||
("CPU时间", "time"),
|
||||
("命令", "comm"),
|
||||
]
|
||||
# 渲染上限:500 行采集但表格最多显示 N 行(树形不受限)
|
||||
MAX_RENDER_ROWS = 200
|
||||
|
||||
def _apply_proc_filter(self):
|
||||
"""根据搜索框 + 排序 + 视图模式 + CPU 过滤,刷新进程表和 TOP 5"""
|
||||
if not hasattr(self, "proc_table"):
|
||||
return
|
||||
# 优化:500 行表格重建时阻塞信号 + 暂停更新,结束后再统一刷新
|
||||
self.proc_table.blockSignals(True)
|
||||
self.proc_table.setUpdatesEnabled(False)
|
||||
try:
|
||||
self._apply_proc_filter_impl()
|
||||
finally:
|
||||
self.proc_table.setUpdatesEnabled(True)
|
||||
self.proc_table.blockSignals(False)
|
||||
self.proc_table.viewport().update()
|
||||
|
||||
def _apply_proc_filter_impl(self):
|
||||
query = self.proc_search.text().strip().lower() if hasattr(self, "proc_search") else ""
|
||||
terms = [t for t in query.split() if t]
|
||||
sort_idx = self.proc_sort_combo.currentIndex() if hasattr(self, "proc_sort_combo") else 0
|
||||
@@ -821,6 +910,10 @@ class MonitorPanel(QWidget):
|
||||
elif sort_idx == 5: # 命令名
|
||||
data.sort(key=lambda p: (p.get("comm", "").split()[0] if p.get("comm") else "", -p.get("pcpu", 0)))
|
||||
|
||||
# 渲染上限:扁平视图截断到 MAX_RENDER_ROWS,避免 setRowCount 500 太慢
|
||||
if not is_tree and len(data) > self.MAX_RENDER_ROWS:
|
||||
data = data[:self.MAX_RENDER_ROWS]
|
||||
|
||||
# 树形视图:按 PPID 排序 + 缩进
|
||||
if is_tree:
|
||||
data = self._build_tree_view(data)
|
||||
@@ -868,8 +961,13 @@ class MonitorPanel(QWidget):
|
||||
total = len(self._proc_data)
|
||||
shown = len(data)
|
||||
mode_label = "树形" if is_tree else "扁平"
|
||||
if terms or min_cpu > 0 or sort_idx != 0 or is_tree:
|
||||
self.proc_group.setTitle(f"进程列表 · {mode_label} · 显示 {shown}/{total}")
|
||||
# 如果渲染被截断,标 "(渲染 top N)"
|
||||
truncated = (not is_tree) and total > self.MAX_RENDER_ROWS and not terms and not min_cpu
|
||||
if terms or min_cpu > 0 or sort_idx != 0 or is_tree or truncated:
|
||||
suffix = ""
|
||||
if truncated and shown == self.MAX_RENDER_ROWS:
|
||||
suffix = f" (top {self.MAX_RENDER_ROWS}, 共 {total})"
|
||||
self.proc_group.setTitle(f"进程列表 · {mode_label} · {shown}/{total}{suffix}")
|
||||
else:
|
||||
self.proc_group.setTitle(f"进程列表 · {mode_label} · {total} 条")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user