Почему мой простой скрипт секундомера на Python занимает так много ресурсов компьютера?
Я начал использовать скрипт Python для секундомера сегодня и заметил значительное замедление во всех других вещах, которые я открыл (Firefox, Sublime Text, Terminal). Системный монитор говорит мне, что мой скрипт секундомера использует около 24% моего процессора. Кажется странным, что что-то настолько тривиальное использует столько ресурсов.
Могу ли я получить несколько советов о том, как улучшить это? Мне бы очень хотелось, чтобы он работал в фоновом режиме и отслеживал мое время, потраченное на разные вещи.
Вот сценарии:
#! /usr/bin/env python3
import tkinter
import time
import datetime
import numpy as np
import subprocess
class StopWatch(tkinter.Frame):
@classmethod
def main(cls):
tkinter.NoDefaultRoot()
root = tkinter.Tk()
root.title('Stop Watch')
root.resizable(False, False)
root.grid_columnconfigure(0, weight=1)
root.geometry("200x235")
padding = dict(padx=5, pady=5)
widget = StopWatch(root, **padding)
widget.grid(sticky=tkinter.NSEW, **padding)
icon = tkinter.PhotoImage(file='stopwatch.ico')
root.tk.call('wm', 'iconphoto', root._w, icon)
root.mainloop()
def __init__(self, master=None, cnf={}, **kw):
padding = dict(padx=kw.pop('padx', 5), pady=kw.pop('pady', 5))
super().__init__(master, cnf, **kw)
self.grid_columnconfigure(0,weight=1)
self.__total = 0
self.start_time=datetime.datetime.now().strftime("%H:%M")
self.start_date=datetime.datetime.now().strftime("%m/%d/%Y")
self.start_dt=tkinter.StringVar(self, self.start_time+" "+self.start_date)
self.__label = tkinter.Label(self, text='Session Time:')
self.__time = tkinter.StringVar(self, '00:00')
self.__display = tkinter.Label(self, textvariable=self.__time,font=(None, 26),height=2)
self.__button = tkinter.Button(self, text='Start', relief=tkinter.RAISED, bg='#008000', activebackground="#329932", command=self.__click)
self.__record = tkinter.Button(self, text='Record', relief=tkinter.RAISED, command=self.__save)
self.__startdt = tkinter.Label(self, textvariable=self.start_dt)
self.__label.grid (row=0, column=0, sticky=tkinter.NSEW, **padding)
self.__display.grid (row=1, column=0, sticky=tkinter.NSEW, **padding)
self.__button.grid (row=2, column=0, sticky=tkinter.NSEW, **padding)
self.__record.grid (row=3, column=0, sticky=tkinter.NSEW, **padding)
self.__startdt.grid (row=4, column=0, sticky=tkinter.N, **padding)
def __click(self):
if self.__total==0:
self.start_time=datetime.datetime.now().strftime("%H:%M")
self.start_date=datetime.datetime.now().strftime("%m/%d/%Y")
self.__time.set(self.start_time+" "+self.start_date)
if self.__button['text'] == 'Start':
self.__button['text'] = 'Stop'
self.__button['bg']='#ff0000'
self.__button['activebackground']='#ff3232'
self.__record['text']='Record'
self.__record['state']='disabled'
self.__record['relief']=tkinter.SUNKEN
self.__start = time.clock()
self.__counter = self.after_idle(self.__update)
else:
self.__button['text'] = 'Start'
self.__button['bg']='#008000'
self.__button['activebackground']='#329932'
self.__record['state']='normal'
self.__record['relief']=tkinter.RAISED
self.after_cancel(self.__counter)
def __save(self):
duration = int(self.__total//60)
if duration > 0:
subprocess.call("cp test_data.dat ./backup", shell=True)
data = np.loadtxt('test_data.dat', dtype="str")
time_data = data[:, 0]
date_data = data[:, 1]
duration_data = data[:, 2]
time_data=np.append(time_data,self.start_time)
date_data=np.append(date_data,self.start_date)
duration_data=np.append(duration_data,str(duration))
new_data=np.column_stack((time_data,date_data,duration_data))
np.savetxt('test_data.dat', new_data, header="*Time* | *Date* | *Duration*", fmt="%s")
self.__record['text']='Saved'
else:
self.__record['text']='Not Saved'
self.start_time=datetime.datetime.now().strftime("%H:%M")
self.start_date=datetime.datetime.now().strftime("%m/%d/%Y")
self.__time.set(self.start_time+" "+self.start_date)
self.__total=0
self.__time.set('00:00')
self.__record['state']='disabled'
self.__record['relief']=tkinter.SUNKEN
def __update(self):
now = time.clock()
diff = now - self.__start
self.__start = now
self.__total += diff
mins,secs=divmod(self.__total,60)
self.__time.set('{:02.0f}:{:02.0f}'.format(mins,secs))
self.start_dt.set(datetime.datetime.now().strftime("%H:%M %m/%d/%Y"))
self.__counter = self.after_idle(self.__update)
if __name__ == '__main__':
StopWatch.main()
2 ответа
Как не дать процессору сойти с ума по времени опроса
В вашем фрагменте:
def __update(self):
now = time.clock()
diff = now - self.__start
self.__start = now
self.__total += diff
mins,secs=divmod(self.__total,60)
self.__time.set('{:02.0f}:{:02.0f}'.format(mins,secs))
self.start_dt.set(datetime.datetime.now().strftime("%H:%M %m/%d/%Y"))
self.__counter = self.after_idle(self.__update)
Функция перезапускается на холостом ходу без каких-либо ограничений. Это означает, что ваш процессор будет тратить каждый момент простоя, чтобы обновить время. Это приведет к загрузке процессора почти на 100%. Поскольку он использует только одно из четырех ядер, вы увидите свои (почти) 25%.
Просто используйте "умный", переменный цикл while; принцип
Если бы мы использовали time.sleep()
, поскольку мы не используем реальное время процессора, у нас будет небольшое отклонение. Процессору всегда нужно немного времени для обработки команды, поэтому
time.sleep(1)
на самом деле будет что-то вроде
time.sleep(1.003)
Это, без дальнейших действий, приведет к накоплению отклонения, однако:
Мы можем сделать процесс умным. В настольных приложениях я всегда выполняю калибровку sleep()
через каждую секунду или минуту, в зависимости от требуемой точности. То, что цикл использует как время для обработки, отводится от следующего цикла, поэтому накопление отклонений никогда не происходит.
В принципе:
import time
seconds = 0 # starttime (displayed)
startt = time.time() # real starttime
print("seconds:", seconds)
wait = 1
while True:
time.sleep(wait)
seconds = seconds + 1 # displayed time (seconds)
target = startt + seconds # the targeted time
real = time.time() # the "real" time
calibration = real - target # now fix the difference between real and targeted
nextwait = 1 - calibration # ...and retract that from the sleep of 1 sec
wait = nextwait if nextwait >= 0 else 1 # prevent errors in extreme situation
print("correction:", calibration)
print("seconds:", seconds)
Поскольку вы используете секунды как единое целое, этого достаточно. Дополнительное бремя: неизмеримо.
Запустив этот фрагмент в терминале, вы увидите отображаемое время и фиксированное отклонение:
seconds: 0
correction: 0.02682352066040039
seconds: 1
correction: 0.036485910415649414
seconds: 2
correction: 0.06434035301208496
seconds: 3
correction: 0.07763338088989258
seconds: 4
correction: 0.037987709045410156
seconds: 5
correction: 0.03364992141723633
seconds: 6
correction: 0.07647705078125
seconds: 7
Использование after() вместо while?
Аналогично, вы можете использовать метод Tkinters after(), как описано здесь, используя тот же трюк с переменным временем для калибровки.
РЕДАКТИРОВАТЬ
По запросу: пример использования метода Tkinter after()
Если вы используете фиксированное время цикла, вы:
- Это неизбежно приводит к потере ресурсов, поскольку время цикла (разрешение по времени) должно составлять небольшую часть отображаемой единицы времени.
- Даже если вы сделаете это, как и ваши 200 мс, отображаемое время будет время от времени показывать разницу с реальным временем (почти) 200 мс, после чего последует слишком короткий переход к следующей отображаемой секунде.
Если вы используете after()
и хотите использовать переменный временной цикл, как в приведенном выше примере без графического интерфейса ниже примера, предлагающего те же параметры, что и фрагмент кода в вашем ответе:
#!/usr/bin/env python3
from tkinter import *
import time
class TestWhile:
def __init__(self):
# state on startup, run or not, initial wait etc
self.run = False
self.showtime = 0
self.wait = 1000
# window stuff
self.window = Tk()
shape = Canvas(width=200, height=0).grid(column=0, row=0)
self.showtext = Label(text="00:00:00", font=(None, 26))
self.showtext.grid(column=0, row=1)
self.window.minsize(width=200, height=50)
self.window.title("Test 123(4)")
# toggle button Run/Stop
self.togglebutton = Button(text="Start", command = self.toggle_run)
self.togglebutton.grid(column=0, row=2, sticky=NSEW, padx=5, pady=5)
self.resetbutton = Button(text="reset", command = self.reset)
self.resetbutton.grid(column=0, row=3, sticky=NSEW, padx=5, pady=5)
self.window.mainloop()
def format_seconds(self, seconds):
mins, secs = divmod(seconds, 60)
hrs, mins = divmod(mins, 60)
return '{:02d}:{:02d}:{:02d}'.format(hrs, mins, secs)
def reset(self):
self.showtime = 0
self.showtext.configure(text="00:00:00")
def toggle_run(self):
# toggle run
if self.run:
self.run = False
self.togglebutton.configure(text="Run")
self.showtime = self.showtime - 1
self.resetbutton.configure(state=NORMAL)
else:
self.run = True
self.togglebutton.configure(text="Stop")
self.resetbutton.configure(state=DISABLED)
# prepare loop, set values etc
self.showtext.configure(text=self.format_seconds(self.showtime))
self.fix = self.showtime
self.starttime = time.time()
# Let's set the first cycle to one second
self.window.after(self.wait, self.fakewhile)
def update(self):
self.window.after(self.wait, self.fakewhile)
self.showtext.configure(text=str(self.format_seconds(self.showtime)))
self.targeted_time = self.starttime + self.showtime
self.realtime = time.time() + self.fix
diff = self.realtime - self.targeted_time
self.wait = int((1 - diff) * 1000)
print("next update after:", self.wait, "ms")
def fakewhile(self):
self.showtime = self.showtime + 1
if self.run:
self.update()
TestWhile()
Заметка
... что если вы обновляете GUI из второго потока, например, в приложении Gtk, вам всегда нужно обновлять с простоя.
Спасибо Джейкоб Влейм за руководство.
Я попытался включить метод time.sleep() в предыдущий фрагмент кода. Это не сработало вообще. Поэтому я обратился к методу tkinter after() и полностью переписал код. Я оставлю суть здесь для того, кто придет и наткнется на эту нить.
Использование метода after () и сценария let в течение 200 мс, прежде чем снова вызывать функцию, освобождает мой процессор и по-прежнему допускает прилично плавный секундомер.
РЕДАКТИРОВАТЬ: удалить лишние плохие коды. См. Комментарий Джейкоба выше, если вы находитесь в одном и том же поиске работы сценариев таймера с tkinter.