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 没有必要在每个周期都使用它们;
您的代码可能的重新实现和简化如下:
- 在
__init__
中创建一个正常的 QTimer,它会更新进程列表(由于明显的原因可能会更改)并更新它;
- 创建一个空字典,将所有键设置为空列表;
- 使用预定义的列数和水平标签设置 table;
- 创建一个循环遍历进程的函数,如果已经存在,则更新其数据,否则创建一个新行并附加新数据;
- 优化函数以改善其执行时间和内存使用;
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 用法并将调整大小留给用户);
我正在创建一个显示 运行 进程的 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 没有必要在每个周期都使用它们;
您的代码可能的重新实现和简化如下:
- 在
__init__
中创建一个正常的 QTimer,它会更新进程列表(由于明显的原因可能会更改)并更新它; - 创建一个空字典,将所有键设置为空列表;
- 使用预定义的列数和水平标签设置 table;
- 创建一个循环遍历进程的函数,如果已经存在,则更新其数据,否则创建一个新行并附加新数据;
- 优化函数以改善其执行时间和内存使用;
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 用法并将调整大小留给用户);