Saturday, May 18, 2024
6
rated 0 times [  6] [ 0]  / answers: 1 / hits: 1090  / 2 Years ago, thu, january 13, 2022, 9:57:39

I started using a Python script for a stopwatch today and noticed a significant slow down in all the other things I have opened (Firefox, Sublime Text, Terminal). System Monitor is telling me my stopwatch script is using about 24% of my CPU. Seems odd that something so trivial uses that much resource.



Can I please get some pointers on how to improve this? I'd really like to run it in the background and keep track of my time spent on various things.



Here is the scripts:



#! /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()


More From » command-line

 Answers
5

How to prevent the processor from going nuts on polling time



In your snippet:



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)


You have the function rerun itself on idle without any limitation. That means your processor will spend each and every moment on idle to update the time. This will lead to a processor load of nearly 100%. Since it uses only one out of four cores, you'll see your (nearly) 25%.



Simply use a "smart", variable while loop; the principle



If we'd use time.sleep(), since we are not using real processor clock time, we would have a slight deviation. The processor always needs a little time to process the command, so



time.sleep(1)


will actually be something like



time.sleep(1.003)


This would, without further actions, lead to accumulating deviation, however:



We can make the process smart. What I always do in desktop applications is to calibrate the sleep() after each second or minute, depending on the required precision. What a cycle uses as time to process is retracted from the next cycle, so there is never an accumulation of deviation.



In principle:



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)


Since you are using seconds as a unit, this seems sufficient. The additional burden: unmeasureable.



Running this snippet in terminal, you'll see both the displayed time and the fixed deviation:



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


Using after() instead of while?



Likewise, you can use Tkinters after() method, as described here, using the same trick with variable time to calibrate.






EDIT



On request: example using Tkinter's after() method



If you use a fixed looptime, you are:




  1. unavoidably waisting resources, since your loop time (time resolution) needs to be a small fraction of the displayed time unit.

  2. Even if you do, like your 200 ms, the displayed time will at times show a difference with real time of (nearly) 200ms, subsequently followed by a much too short jump to the next displayed second.



If you use after(), and want to use a variable time cycle, like in the non-gui example above, below an example, offering the exact same options as the snippet in your answer:



enter image description here



#!/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()


Note



...that if you are updating GUI from a second thread in e.g. a Gtk application, you'd always need to update from idle.


[#5550] Saturday, January 15, 2022, 2 Years  [reply] [flag answer]
Only authorized users can answer the question. Please sign in first, or register a free account.
poous

Total Points: 81
Total Questions: 101
Total Answers: 119

Location: Cambodia
Member since Sat, Oct 3, 2020
4 Years ago
;