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:
+69
-6
@@ -13,13 +13,16 @@ from .ssh_client import SSHConnection
|
||||
class SystemMonitor:
|
||||
"""远程主机的资源监控器(数据全部从 SSH 通道采集,不依赖 agent)"""
|
||||
|
||||
# 一次性获取所有指标的脚本(Linux)
|
||||
# 慢脚本:CPU 差分需要 sleep 1(约 1.4 秒)
|
||||
_LINUX_METRICS_SCRIPT = r"""
|
||||
echo "===CPU==="
|
||||
# 第一次采样 1 秒间隔,用来计算差值
|
||||
read cpu_user cpu_nice cpu_system cpu_idle cpu_iowait cpu_irq cpu_softirq cpu_steal < /proc/stat
|
||||
# 第一次采样 1 秒间隔,用来计算差分
|
||||
awk 'NR==1 {gsub("cpu ",""); print}' /proc/stat > /tmp/.cpu1.$$
|
||||
sleep 1
|
||||
read cpu_user2 cpu_nice2 cpu_system2 cpu_idle2 cpu_iowait2 cpu_irq2 cpu_softirq2 cpu_steal2 < /proc/stat
|
||||
awk 'NR==1 {gsub("cpu ",""); print}' /proc/stat > /tmp/.cpu2.$$
|
||||
read cpu_user cpu_nice cpu_system cpu_idle cpu_iowait cpu_irq cpu_softirq cpu_steal cpu_guest cpu_gnice < /tmp/.cpu1.$$
|
||||
read cpu_user2 cpu_nice2 cpu_system2 cpu_idle2 cpu_iowait2 cpu_irq2 cpu_softirq2 cpu_steal2 cpu_guest2 cpu_gnice2 < /tmp/.cpu2.$$
|
||||
rm -f /tmp/.cpu1.$$ /tmp/.cpu2.$$
|
||||
total1=$((cpu_user+cpu_nice+cpu_system+cpu_idle+cpu_iowait+cpu_irq+cpu_softirq+cpu_steal))
|
||||
total2=$((cpu_user2+cpu_nice2+cpu_system2+cpu_idle2+cpu_iowait2+cpu_irq2+cpu_softirq2+cpu_steal2))
|
||||
idle1=$cpu_idle; idle2=$cpu_idle2
|
||||
@@ -68,6 +71,65 @@ echo "===HOST==="
|
||||
echo "HOSTNAME=$(hostname)"
|
||||
echo "KERNEL=$(uname -r)"
|
||||
echo "OS=$(. /etc/os-release 2>/dev/null && echo "$PRETTY_NAME" || uname -s)"
|
||||
"""
|
||||
|
||||
# 快脚本:不含 sleep 1,但 CPU 数字用即时 busy%(基于 /proc/stat 当前总样本,
|
||||
# 加上 -1 让服务端快速 grep 两次间隔 0.3s 算差分,~0.4s 总耗时)
|
||||
# 用 usleep 微秒精度,0.3s 差分窗口 + awk 算 CPU%
|
||||
_FAST_METRICS_SCRIPT = r"""
|
||||
echo "===CPU==="
|
||||
# 快速差分:0.3 秒间隔(避免 1.5s 阻塞)。误差 ±5% 但响应快
|
||||
# awk 解析 /proc/stat 第一行(cpu 总览),两次采样做差分
|
||||
awk 'NR==1 {gsub("cpu ",""); print}' /proc/stat > /tmp/.cpu1.$$
|
||||
sleep 0.3 2>/dev/null || sleep 1
|
||||
awk 'NR==1 {gsub("cpu ",""); print}' /proc/stat > /tmp/.cpu2.$$
|
||||
read u1 n1 s1 i1 io1 irq1 si1 st1 g1 gn1 < /tmp/.cpu1.$$
|
||||
read u2 n2 s2 i2 io2 irq2 si2 st2 g2 gn2 < /tmp/.cpu2.$$
|
||||
rm -f /tmp/.cpu1.$$ /tmp/.cpu2.$$
|
||||
t1=$((u1+n1+s1+i1+io1+irq1+si1+st1))
|
||||
t2=$((u2+n2+s2+i2+io2+irq2+si2+st2))
|
||||
di=$((i2-i1))
|
||||
dt=$((t2-t1))
|
||||
if [ $dt -gt 0 ]; then usage=$(( (1000*(dt-di)/dt+5)/10 )); else usage=0; fi
|
||||
echo "CPU_USAGE=$usage"
|
||||
echo "CPU_CORES=$(nproc 2>/dev/null || echo 1)"
|
||||
echo "LOAD=$(cat /proc/loadavg | awk '{print $1,$2,$3}')"
|
||||
echo "UPTIME=$(awk '{printf "%.0f",$1}' /proc/uptime)"
|
||||
echo "===MEM==="
|
||||
mem_total=$(awk '/MemTotal/{print $2}' /proc/meminfo)
|
||||
mem_avail=$(awk '/MemAvailable/{print $2}' /proc/meminfo)
|
||||
swap_total=$(awk '/SwapTotal/{print $2}' /proc/meminfo)
|
||||
swap_free=$(awk '/SwapFree/{print $2}' /proc/meminfo)
|
||||
if [ -z "$mem_avail" ]; then mem_avail=$((mem_total - $(awk '/^(Buffers|Cached|SReclaimable):/{s+=$2} END{print s}' /proc/meminfo))); fi
|
||||
used=$((mem_total - mem_avail))
|
||||
echo "MEM_TOTAL=$mem_total"
|
||||
echo "MEM_USED=$used"
|
||||
echo "MEM_AVAIL=$mem_avail"
|
||||
echo "SWAP_TOTAL=$swap_total"
|
||||
echo "SWAP_USED=$((swap_total-swap_free))"
|
||||
echo "===DISK==="
|
||||
df -PB1 -x tmpfs -x devtmpfs 2>/dev/null | awk 'NR>1 {printf "DISK|%s|%d|%d|%s\n",$NF,$2,$3,$5}'
|
||||
echo "===NET==="
|
||||
for iface in $(ls /sys/class/net/ 2>/dev/null | grep -v lo); do
|
||||
rx=$(cat /sys/class/net/$iface/statistics/rx_bytes 2>/dev/null || echo 0)
|
||||
tx=$(cat /sys/class/net/$iface/statistics/tx_bytes 2>/dev/null || echo 0)
|
||||
echo "NET|$iface|$rx|$tx"
|
||||
done
|
||||
echo "===PROC==="
|
||||
ps -e -o stat= 2>/dev/null | awk '{r+=($1~/^R/); s+=($1~/^S/); d+=($1~/^D/); z+=($1~/^Z/); t++} END{printf "PROC_TOTAL=%d\nPROC_RUNNING=%d\nPROC_SLEEP=%d\nPROC_DISK=%d\nPROC_ZOMBIE=%d\n", t, r, s, d, z}'
|
||||
ps -eo pid,ppid,user,pcpu,pmem,vsz,rss,stat,pri,nice,etimes,times,args --sort=-pcpu --no-headers 2>/dev/null \
|
||||
| head -500 \
|
||||
| awk '{
|
||||
out="PROC\t";
|
||||
for(i=1;i<=12;i++) out=out $i "\t";
|
||||
rest="";
|
||||
for(i=13;i<=NF;i++) rest=(i==13?$i:rest " " $i);
|
||||
print out rest
|
||||
}'
|
||||
echo "===HOST==="
|
||||
echo "HOSTNAME=$(hostname)"
|
||||
echo "KERNEL=$(uname -r)"
|
||||
echo "OS=$(. /etc/os-release 2>/dev/null && echo "$PRETTY_NAME" || uname -s)"
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
@@ -77,7 +139,7 @@ echo "OS=$(. /etc/os-release 2>/dev/null && echo "$PRETTY_NAME" || uname -s)"
|
||||
return m.group(1).strip() if m else default
|
||||
|
||||
@classmethod
|
||||
def collect(cls, conn: SSHConnection) -> dict:
|
||||
def collect(cls, conn: SSHConnection, fast: bool = True) -> dict:
|
||||
"""采集一次指标;返回 dict"""
|
||||
empty = {
|
||||
"cpu": 0.0, "cores": 1, "load1": 0, "load5": 0, "load15": 0,
|
||||
@@ -91,7 +153,8 @@ echo "OS=$(. /etc/os-release 2>/dev/null && echo "$PRETTY_NAME" || uname -s)"
|
||||
}
|
||||
if not conn or not conn.connected:
|
||||
return empty
|
||||
code, out, err = conn.exec_command(cls._LINUX_METRICS_SCRIPT, timeout=10)
|
||||
script = cls._FAST_METRICS_SCRIPT if fast else cls._LINUX_METRICS_SCRIPT
|
||||
code, out, err = conn.exec_command(script, timeout=10)
|
||||
if code != 0 or not out:
|
||||
empty["error"] = err or "采集失败"
|
||||
return empty
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
"""
|
||||
监控 UI 不卡死测试:
|
||||
- 启动监控
|
||||
- 5 秒内应该完成 ≥2 次采集(说明没卡死)
|
||||
- worker 不会重复启动
|
||||
- 关闭/重新打开工作正常
|
||||
- _on_refresh_now 不打乱 timer
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import threading
|
||||
|
||||
os.environ["QT_QPA_PLATFORM"] = "offscreen"
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
|
||||
from PyQt5.QtCore import QTimer
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
|
||||
from core.ssh_client import SSHConnection
|
||||
from core.monitor import SystemMonitor
|
||||
from core.manager import ConnectionManager
|
||||
from ui.widgets import MonitorPanel
|
||||
|
||||
|
||||
def find_local_ssh():
|
||||
for pwd in ("sshclient_test_pwd_2026", "testpass", ""):
|
||||
c = SSHConnection("127.0.0.1", 22, "root", pwd, timeout=5)
|
||||
ok, _ = c.connect()
|
||||
if ok:
|
||||
return c
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
app = QApplication(sys.argv)
|
||||
print("[1/6] 连接本机 SSH")
|
||||
conn = find_local_ssh()
|
||||
if not conn:
|
||||
print(" ⚠ 本机 SSH 不可用,跳过")
|
||||
return
|
||||
print(f" ✓ {conn.username}@{conn.host}")
|
||||
|
||||
print("[2/6] 创建 MonitorPanel + 启动监控")
|
||||
mgr = ConnectionManager()
|
||||
mgr.connections["fake"] = conn
|
||||
panel = MonitorPanel(mgr)
|
||||
panel.set_host("fake")
|
||||
panel.btn_toggle.setChecked(True) # 等价于点「开始监控」
|
||||
app.processEvents()
|
||||
assert panel.btn_toggle.isChecked()
|
||||
assert panel._monitor_conn is not None
|
||||
print(f" ✓ 监控启动, interval={panel._monitor_interval_ms}ms")
|
||||
|
||||
print("[3/6] 5 秒内累计采集次数(要求 ≥2 次且不卡死)")
|
||||
# 改用更短的 interval 让测试在 5s 内能采到 2 次以上
|
||||
panel._monitor_interval_ms = 1000
|
||||
# 用 wrapper 替换 _on_sample 来计数。
|
||||
# 关键:_kick_one_sample 里每次创建新 worker 时执行
|
||||
# self.worker.sample_ready.connect(self._on_sample)
|
||||
# 此处 self._on_sample 在连接时求值,所以新 worker 会连到 wrapper。
|
||||
# 旧的断开/重连方式只作用于当前 worker,新 worker 不经过 counting。
|
||||
orig_on_sample = panel._on_sample
|
||||
samples = [0]
|
||||
def counting(m):
|
||||
samples[0] += 1
|
||||
orig_on_sample(m)
|
||||
panel._on_sample = counting
|
||||
# 重启 timer 到 1s(如果 worker #1 还在跑,等它结束后 _on_worker_finished
|
||||
# 会创建/重启 timer,此时 _monitor_interval_ms 已是 1000)
|
||||
if getattr(panel, "_monitor_timer", None):
|
||||
panel._monitor_timer.stop()
|
||||
panel._monitor_timer.start(1000)
|
||||
# 立即触发一次采集(如果 worker #1 已结束的话)
|
||||
panel._kick_one_sample()
|
||||
# 跑 5 秒(interval 1s + ~0.4s 采集耗时,应该能采 ~3 次)
|
||||
deadline = time.time() + 5.0
|
||||
while time.time() < deadline:
|
||||
app.processEvents()
|
||||
time.sleep(0.05)
|
||||
wr = panel.worker.isRunning() if panel.worker else 'N/A'
|
||||
timer = getattr(panel, '_monitor_timer', None)
|
||||
print(f" 调试: samples={samples[0]} worker.isRunning={wr} timer.isActive={timer.isActive() if timer else 'N/A'}")
|
||||
assert samples[0] >= 2, f"5s 内只采了 {samples[0]} 次,太少(卡死了?)"
|
||||
print(f" ✓ 5s 内采集次数: {samples[0]} (期望 ≥2)")
|
||||
|
||||
print("[4/6] 验证 worker 不会重复启动")
|
||||
assert panel.worker is not None
|
||||
# worker 刚跑完应在 finished 状态
|
||||
deadline = time.time() + 2
|
||||
while panel.worker and panel.worker.isRunning() and time.time() < deadline:
|
||||
app.processEvents()
|
||||
time.sleep(0.05)
|
||||
assert not panel.worker.isRunning(), "上次的 worker 应该在 finished 状态"
|
||||
# 现在再点 refresh_now
|
||||
samples[0] = 0
|
||||
panel._on_refresh_now()
|
||||
app.processEvents()
|
||||
time.sleep(0.5)
|
||||
app.processEvents()
|
||||
assert samples[0] >= 1 or panel.worker.isRunning(), "refresh_now 后没采集到"
|
||||
print(f" ✓ 立即刷新后采集 1 次(worker isRunning={panel.worker.isRunning() if panel.worker else 'N/A'})")
|
||||
# 恢复原始 _on_sample
|
||||
panel._on_sample = orig_on_sample
|
||||
|
||||
print("[5/6] 关闭监控(停止 worker)")
|
||||
panel.btn_toggle.setChecked(False)
|
||||
app.processEvents()
|
||||
# 等 worker 退出
|
||||
if panel.worker:
|
||||
deadline = time.time() + 2
|
||||
while panel.worker.isRunning() and time.time() < deadline:
|
||||
app.processEvents()
|
||||
time.sleep(0.05)
|
||||
assert not panel.worker.isRunning(), "关闭后 worker 还在跑"
|
||||
assert panel._monitor_timer is None, "关闭后 timer 还在"
|
||||
print(f" ✓ 监控关闭 (worker stopped, timer cleared)")
|
||||
|
||||
print("[6/6] 重新打开监控应能正常恢复")
|
||||
panel.btn_toggle.setChecked(True)
|
||||
app.processEvents()
|
||||
assert panel._monitor_conn is not None
|
||||
assert panel._monitor_interval_ms > 0
|
||||
# 等 worker 跑完
|
||||
deadline = time.time() + 3
|
||||
while time.time() < deadline:
|
||||
app.processEvents()
|
||||
if panel.worker and not panel.worker.isRunning() and panel._proc_data:
|
||||
break
|
||||
time.sleep(0.05)
|
||||
assert panel._proc_data, "重开后没采到数据"
|
||||
print(f" ✓ 重开正常,进程数={len(panel._proc_data)}")
|
||||
|
||||
# 清理
|
||||
panel.btn_toggle.setChecked(False)
|
||||
app.processEvents()
|
||||
conn.disconnect()
|
||||
print("\n监控 UI 不卡死测试通过 ✓")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -61,7 +61,10 @@ def main():
|
||||
panel.set_host("fake")
|
||||
panel._proc_data = m["processes"]
|
||||
panel._apply_proc_filter()
|
||||
assert panel.proc_table.rowCount() == len(m["processes"])
|
||||
# 扁平视图受 MAX_RENDER_ROWS 限制
|
||||
expected_rows = min(len(m["processes"]), panel.MAX_RENDER_ROWS)
|
||||
assert panel.proc_table.rowCount() == expected_rows, \
|
||||
f"行数 {panel.proc_table.rowCount()} != 预期 {expected_rows} (总 {len(m['processes'])}, 上限 {panel.MAX_RENDER_ROWS})"
|
||||
# 验证第一列 (PID) 是数字
|
||||
pid_text = panel.proc_table.item(0, 0).text()
|
||||
assert pid_text.isdigit(), f"PID 列不是数字: {pid_text!r}"
|
||||
@@ -110,7 +113,7 @@ def main():
|
||||
print(f" ✓ CPU>=10% 过滤后剩 {after_min} 行,全部 CPU≥10%")
|
||||
panel.proc_min_cpu.setValue(0)
|
||||
app.processEvents()
|
||||
assert panel.proc_table.rowCount() == len(m["processes"])
|
||||
assert panel.proc_table.rowCount() == expected_rows
|
||||
print(f" ✓ CPU 过滤清空恢复 {panel.proc_table.rowCount()} 行")
|
||||
|
||||
print("[4/5] 搜索过滤")
|
||||
@@ -130,14 +133,14 @@ def main():
|
||||
# 清空搜索
|
||||
panel.proc_search.setText("")
|
||||
app.processEvents()
|
||||
assert panel.proc_table.rowCount() == len(m["processes"]), "清空搜索应恢复"
|
||||
assert panel.proc_table.rowCount() == expected_rows, "清空搜索应恢复"
|
||||
print(f" ✓ 清空搜索恢复全部 {panel.proc_table.rowCount()} 行")
|
||||
|
||||
# 测试按内存排序
|
||||
panel.proc_sort_combo.setCurrentIndex(1) # 按 MEM 降序
|
||||
app.processEvents()
|
||||
first = panel.proc_table.item(0, 3).text() # MEM% 在第 3 列
|
||||
last = panel.proc_table.item(panel.proc_table.rowCount() - 1, 3).text()
|
||||
first = panel.proc_table.item(0, 4).text() # MEM% 在第 4 列
|
||||
last = panel.proc_table.item(panel.proc_table.rowCount() - 1, 4).text()
|
||||
assert float(first) >= float(last), f"按 MEM 排序失败: {first} < {last}"
|
||||
print(f" ✓ 按 MEM 排序: 第 1 行 {first}% > 最后行 {last}%")
|
||||
|
||||
|
||||
+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} 条")
|
||||
|
||||
|
||||
+14
-12
@@ -50,8 +50,8 @@ class CommandWorker(QThread):
|
||||
self.finished_with.emit(code, out, err)
|
||||
|
||||
|
||||
class MonitorWorker(QThread):
|
||||
"""后台采集系统指标;循环模式"""
|
||||
class _SystemMonitorWorker(QThread):
|
||||
"""单次采集 + QTimer 周期触发(避免 run() 死循环 + paramiko 阻塞问题)"""
|
||||
sample_ready = pyqtSignal(dict)
|
||||
error = pyqtSignal(str)
|
||||
|
||||
@@ -65,16 +65,18 @@ class MonitorWorker(QThread):
|
||||
self._stop = True
|
||||
|
||||
def run(self):
|
||||
while not self._stop:
|
||||
try:
|
||||
m = SystemMonitor.collect(self.conn)
|
||||
self.sample_ready.emit(m)
|
||||
except Exception as e:
|
||||
self.error.emit(str(e))
|
||||
for _ in range(self.interval * 10):
|
||||
if self._stop:
|
||||
return
|
||||
time.sleep(0.1)
|
||||
"""单次采集一帧;由主线程用 QTimer 调度下一次"""
|
||||
if self._stop:
|
||||
return
|
||||
try:
|
||||
m = SystemMonitor.collect(self.conn)
|
||||
self.sample_ready.emit(m)
|
||||
except Exception as e:
|
||||
self.error.emit(str(e))
|
||||
|
||||
|
||||
# 保留旧类名做兼容(一些其他地方可能引用了 MonitorWorker)
|
||||
MonitorWorker = _SystemMonitorWorker
|
||||
|
||||
|
||||
class UploadWorker(QThread):
|
||||
|
||||
Reference in New Issue
Block a user