QTimer.singleshot 导致 GUI 挂起

QTimer.singleshot is causing the GUI to hangout

我正在创建一个显示 运行 进程的 table,并使用装饰器来更新这些信息。在下面的代码中使用装饰器,导致 GUI 在每次调用 singleshot 时挂起(每秒)。

为什么 singleshot 导致 GUI 挂起,我怎样才能获得更好的逻辑?

# First create table
data = getProcesses()
tableWidget = QTableWidget()
Layout.addWidget(tableWidget)
fillTable(data, len(data['pid']), len(data), tableWidget)

# get the processes
    def getProcesses():
        allprocesses = {}
        for p in psutil.process_iter():
            try:
                if p.name().lower() in ["python.exe", "pythonw.exe"]: # console, window
                    with p.oneshot():
                        allprocesses.setdefault('pid', []).append(p.pid)
                        allprocesses.setdefault('memory(MB)', []).append(p.memory_full_info().uss/(1024**2))
                        allprocesses.setdefault('memory(%)', []).append(p.memory_percent(memtype="rss"))
                        allprocesses.setdefault('cpu_times(s)', []).append(sum(p.cpu_times()[:2]))
                        allprocesses.setdefault('create_time', []).append(datetime.datetime.fromtimestamp(p.create_time()).strftime("%Y-%m-%d %H:%M:%S"))
                        allprocesses.setdefault('cpu(%)', []).append(p.cpu_percent()/psutil.cpu_count())
            except:
                continue
        del p
        return allprocesses

def updateInfo(data, table):
    try:
        table.clear()
        for p in psutil.process_iter():
            if p.pid in data['pid']:
                try:
                    with p.oneshot():
                        data['memory(MB)'][data['pid'].index(p.pid)]    = p.memory_full_info().uss/(1024**2)
                        data['memory(%)'][data['pid'].index(p.pid)]     = p.memory_percent(memtype="rss")
                        data['cpu_times(s)'][data['pid'].index(p.pid)]  = sum(p.cpu_times()[:2])
                        data['cpu(%)'][data['pid'].index(p.pid)]        = p.cpu_percent()/psutil.cpu_count()
                        self.fillTable(data, len(data['pid']), len(data), table) 
                except:
                    continue
    except:
        pass                      

def tabledecorator(func):
    @functools.wraps(func)
    def wrapper(data, r, c, table):
        func(data, r, c, table)
        QTimer.singleShot(1000, lambda: self.updateInfo(data, table))            
    return wrapper

@tabledecorator
def fillTable(data, r, c, table):
    table.setRowCount(r) 
    table.setColumnCount(c)
    horHeaders = []
    for n, key in enumerate(reversed(sorted(data.keys()))):
        horHeaders.append(key)
        for m, item in enumerate(data[key]):
            newitem = QTableWidgetItem()
            newitem.setData(Qt.DisplayRole, item)
            table.setItem(m, n, newitem)
    table.setHorizontalHeaderLabels(horHeaders)
    table.resizeColumnsToContents()
    table.resizeRowsToContents()
    del horHeaders, n, key, m, item, newitem

您的实施中存在各种性能问题,但最重要的是 all 项调用了 fillTable

由于该函数用计时器修饰,结果是您将为 table 中的 each 行调用延迟的 updateInfo,由于该函数 again 调用了修饰的 fillTable,您实际上遇到了 巨大的 递归问题:在每个新循环中,函数调用的数量呈指数增长。

如果你有 2 个匹配的进程,第一次调用 updateInfo 时它将调用 fillTable 两次,同时创建两个 QTimer。一秒钟后,您将 两次 调用 updateInfo,结果是 4 次调用(2 个进程乘以 2 次调用 updateInfo);再过一秒钟,它们将是 8,然后是 16,依此类推。

您的代码的另一个问题是,在每次调用 fillTable 时,您都在调用三个函数,每个循环只应执行一次:

  • setHorizontalHeaderLabels;
  • resizeColumnsToContents;
  • resizeRowsToContents;

第一个在 any 情况下毫无意义,因为列标签肯定 not 在程序的生命周期内发生变化,并且当调用该函数时,它会循环遍历 所有 它的 header 项以检查是否有任何更改。
另外两个在性能方面要求很高,因为视图被迫查询 all 项目并调用底层函数来计算每个项目的大小提示行和列,然后相应地调整大小。

为此目的使用单次计时器确实没有意义,因为问题是您依赖函数参数来更新数据,而更好的方法是正确使用 objects 和参考资料。

您实施中的其他性能问题:

  • 因为字典的键是已知的并且不会改变,所以使用 setdefault();
  • 没有意义
  • 您在每个周期不断地检索相同的列表;
  • 您每次都在构建搜索列表(这会耗费时间和内存);
  • 有些值显然是常量,computing/retrieving 没有必要在每个周期都使用它们;

您的代码可能的重新实现和简化如下:

  1. __init__ 中创建一个正常的 QTimer,它会更新进程列表(由于明显的原因可能会更改)并更新它;
  2. 创建一个空字典,将所有键设置为空列表;
  3. 使用预定义的列数和水平标签设置 table;
  4. 创建一个循环遍历进程的函数,如果已经存在,则更新其数据,否则创建一个新行并附加新数据;
  5. 优化函数以改善其执行时间和内存使用;
CpuCount = psutil.cpu_count()
MemoryRatio = 1024 ** 2

class ProcView(QtWidgets.QWidget):
    def __init__(self):
        super().__init__()
        layout = QtWidgets.QVBoxLayout(self)
        self.table = QtWidgets.QTableWidget(0, 6)
        layout.addWidget(self.table)

        self.headers = 'pid', 'memory(MB)', 'memory(%)', 'cpu_times(s)', 'create_time', 'cpu(%)'
        self.updateColumns = 1, 2, 3, 5
        self.table.setHorizontalHeaderLabels(self.headers)

        self.procTimer = QtCore.QTimer(interval=1000, timeout=self.updateInfo)
        self.filter = 'python3', 'python'
        self.procs = {header:[] for header in self.headers}
        self.procTimer.start()
        self.updateInfo()

    def updateInfo(self):
        pids = self.procs['pid']
        memoryMb = self.procs['memory(MB)']
        memoryPerc = self.procs['memory(%)']
        cpu_times = self.procs['cpu_times(s)']
        create_times = self.procs['create_time']
        cpuPerc = self.procs['cpu(%)']
        for p in psutil.process_iter():
            if not p.name().lower() in self.filter:
                continue
            with p.oneshot():
                if p.pid not in pids:
                    row = len(pids)
                    self.table.insertRow(row)
                    pids.append(p.pid)
                    memoryMb.append(p.memory_full_info().uss / MemoryRatio)
                    memoryPerc.append(p.memory_percent(memtype="rss"))
                    cpu_times.append(sum(p.cpu_times()[:2]))
                    create_times.append(datetime.datetime.fromtimestamp(p.create_time()).strftime("%Y-%m-%d %H:%M:%S"))
                    cpuPerc.append(p.cpu_percent() / CpuCount)
                    for col, header in enumerate(self.headers):
                        item = QtWidgets.QTableWidgetItem()
                        item.setData(QtCore.Qt.DisplayRole, self.procs[header][row])
                        self.table.setItem(row, col, item)
                else:
                    row = pids.index(p.pid)
                    memoryMb[row] = p.memory_full_info().uss / MemoryRatio
                    memoryPerc[row] = p.memory_percent(memtype="rss")
                    cpu_times[row] = sum(p.cpu_times()[:2])
                    cpuPerc[row] = p.cpu_percent() / CpuCount
                    for col in self.updateColumns:
                        item = self.table.item(row, col)
                        item.setData(QtCore.Qt.DisplayRole, self.procs[self.headers[col]][row])

        self.table.resizeColumnsToContents()
        self.table.resizeRowsToContents()

请注意,正确的实施可能应该:

  • 避免使用以字段作为键和每个字段的列表的字典,但最终使用 pid 作为键的字典和每个项目字段的字典(一个专门的 class 作为抽象该过程的层会更好);
  • 使用自定义模型(带有 QTableView);
  • 每当进程终止时进行验证;
  • 使用固定行大小并避免在每个周期自动调整列大小所有列(最好对常见的固定大小列使用Fixed调整大小模式,例如CPU 用法并将调整大小留给用户);