3
\$\begingroup\$

I am trying to implement Elapsed Timer in Python (version 3). The clock and time methods in Python seems to have different implementations of calculating time in different platforms. So, I thought of getting the difference between the system time seconds to detect the change in second.

Though the program works as expected, it consumes around 25% of CPU which is way beyond my expectation. I know in an infinite loop I keep on fetching the value for system time which is causing the higher usage.

But, there should be a better way. Since I am new to Python I couldn't think of any better alternate approaches.

#!/usr/bin/python
import tkinter
import tkinter.messagebox
import tkinter.font
import time
import sys 
import _thread
from tkinter import *
from time import strftime
top = tkinter.Tk()
top.wm_attributes("-topmost", 1)
timer_stop = 0 
def startCountDown():
 try:
 _thread.start_new_thread( startTimer , ("Thread-1", 2, ) ) 
 except:
 tkinter.messagebox.showinfo( "Error", "Unable to start thread")
def startTimer( threadName, delay):
 global timer_stop
 time_start = time.time()
 hours = 0
 seconds = -1 
 minutes = 0
 prev_second = 0
 current_second = 0
 var7.set(strftime("%H:"))
 var8.set(strftime("%M:"))
 var9.set(strftime("%S"))
 while True:
 try:
 var1.set(str(hours) + ": ")
 var2.set(str(minutes) + ": ")
 var3.set(str(seconds) + " ")
 var4.set(strftime("%H:"))
 var5.set(strftime("%M:"))
 var6.set(strftime("%S"))
 current_second = strftime("%S")
 if prev_second != current_second:
 seconds += 1
 prev_second = current_second
 if seconds == 60: 
 minutes += 1
 seconds = 0
 if minutes == 60: 
 hours += 1
 minutes = 0 
 if timer_stop == 1:
 timer_stop = 0
 break
 except KeyboardInterrupt:
 break
def stopTimer():
 global timer_stop
 timer_stop = 1
B1 = tkinter.Button(top, text ="Start", command = startCountDown).grid(row=0,column=3);
B2 = tkinter.Button(top, text ="Stop", command = stopTimer).grid(row=1,column=3);
cFont = tkinter.font.Font(family="sans-serif", size=16)
var1 = StringVar()
label1 = Label( top, textvariable=var1, relief=FLAT ,font=cFont).grid(row=0,column=0);
var2 = StringVar()
label2 = Label( top, textvariable=var2, relief=FLAT ,font=cFont).grid(row=0,column=1);
var3 = StringVar()
label3 = Label( top, textvariable=var3, relief=FLAT ,font=cFont).grid(row=0,column=2);
var4 = StringVar()
label4 = Label( top, textvariable=var4, relief=FLAT ,font=cFont).grid(row=1,column=0);
var5 = StringVar()
label5 = Label( top, textvariable=var5, relief=FLAT ,font=cFont).grid(row=1,column=1);
var6 = StringVar()
label6 = Label( top, textvariable=var6, relief=FLAT ,font=cFont).grid(row=1,column=2);
var7 = StringVar()
label7 = Label( top, textvariable=var7, relief=FLAT ,font=cFont).grid(row=2,column=0);
var8 = StringVar()
label8 = Label( top, textvariable=var8, relief=FLAT ,font=cFont).grid(row=2,column=1);
var9 = StringVar()
label9 = Label( top, textvariable=var9, relief=FLAT ,font=cFont).grid(row=2,column=2);
top.title("Elapsed Time")
top.mainloop()

Update: Based on the suggestion from @Simon, the code has been rewritten with better logic. Now the code rarely hits 2% of CPU, rest of the times usage is negligible (0%). Following lists the update code, it would be really helpful if someone can come up with an even better approach:

#!/usr/bin/python
import tkinter
import tkinter.messagebox
import tkinter.font
import time
import sys 
import _thread
from tkinter import *
from time import strftime
top = tkinter.Tk()
top.wm_attributes("-topmost", 1)
timer_stop = 0 
def startCountDown():
 try:
 _thread.start_new_thread( startTimer , ("Thread-1", 2, ) ) 
 except:
 tkinter.messagebox.showinfo( "Error", "Unable to start thread")
def startTimer( threadName, delay):
 global timer_stop
 time_start = time.time()
 hours = 0
 seconds = -1 
 minutes = 0
 prev_second = 0
 current_second = 0
 var7.set(strftime("%H:"))
 var8.set(strftime("%M:"))
 var9.set(strftime("%S"))
 while True:
 try:
 current_second = strftime("%S")
 time.sleep(500/1000.0)
 if prev_second != current_second:
 seconds += 1
 prev_second = current_second
 if seconds == 60: 
 minutes += 1
 seconds = 0
 if minutes == 60: 
 hours += 1
 minutes = 0 
 var1.set(str(hours) + ": ")
 var2.set(str(minutes) + ": ")
 var3.set(str(seconds) + " ")
 var4.set(strftime("%H:"))
 var5.set(strftime("%M:"))
 var6.set(strftime("%S"))
 if timer_stop == 1:
 timer_stop = 0
 break
 except KeyboardInterrupt:
 break
def stopTimer():
 global timer_stop
 timer_stop = 1
B1 = tkinter.Button(top, text ="Start", command = startCountDown).grid(row=0,column=3);
B2 = tkinter.Button(top, text ="Stop", command = stopTimer).grid(row=1,column=3);
cFont = tkinter.font.Font(family="sans-serif", size=16)
var1 = StringVar()
label1 = Label( top, textvariable=var1, relief=FLAT ,font=cFont).grid(row=0,column=0);
var2 = StringVar()
label2 = Label( top, textvariable=var2, relief=FLAT ,font=cFont).grid(row=0,column=1);
var3 = StringVar()
label3 = Label( top, textvariable=var3, relief=FLAT ,font=cFont).grid(row=0,column=2);
var4 = StringVar()
label4 = Label( top, textvariable=var4, relief=FLAT ,font=cFont).grid(row=1,column=0);
var5 = StringVar()
label5 = Label( top, textvariable=var5, relief=FLAT ,font=cFont).grid(row=1,column=1);
var6 = StringVar()
label6 = Label( top, textvariable=var6, relief=FLAT ,font=cFont).grid(row=1,column=2);
var7 = StringVar()
label7 = Label( top, textvariable=var7, relief=FLAT ,font=cFont).grid(row=2,column=0);
var8 = StringVar()
label8 = Label( top, textvariable=var8, relief=FLAT ,font=cFont).grid(row=2,column=1);
var9 = StringVar()
label9 = Label( top, textvariable=var9, relief=FLAT ,font=cFont).grid(row=2,column=2);
top.title("Elapsed Time")
top.mainloop()
asked Nov 14, 2016 at 0:18
\$\endgroup\$
6
  • \$\begingroup\$ Could you do it with 3 labels? \$\endgroup\$ Commented Nov 14, 2016 at 1:09
  • \$\begingroup\$ @Simon Can you please elaborate your question? Labels are mainly used to display the value. \$\endgroup\$ Commented Nov 14, 2016 at 1:21
  • \$\begingroup\$ I was just thinking that you have 9 labels, for what is really is 3 values, or it could be.. Maybe you could just use one row for the entire timestamp? Or is there any reason you chose not to? \$\endgroup\$ Commented Nov 14, 2016 at 1:26
  • \$\begingroup\$ @Simon That's mainly for UI customization. To allow the user to change the font and size of text in each label as the user wishes \$\endgroup\$ Commented Nov 14, 2016 at 1:30
  • \$\begingroup\$ Yes I see. On another note it is cpu intensive because the timer while loop will try to finish as fast as possible, it probably does like a 100 checks every millisecond. Limit it. Maybe use time.sleep to sleep for what time is left to the next update.. \$\endgroup\$ Commented Nov 14, 2016 at 1:38

1 Answer 1

1
\$\begingroup\$

I thought I might as well summit a answer. It will first contain some styling points, and then a async approach to the question.


Styling

  1. In python, the ";" that you use in the end of some of your rows does nothing, python instead uses line breaks and indentation.
  2. You use very many variables, and you probably don't have to, and it seems that you are repeating yourself. If you want to do it that way, there are a better way.
  3. Function naming conventions in python is not camelCase, but instead function_name, camel case is reserved for classes.
  4. variable naming conventions in python is always lower case, and not B1, that looks to me as a poorly named class and not a button.
  5. You should write you variables after your functions, and not into right after your imports.
  6. When you top-down a piece of code, you make it dense, and hard to manipulate and read, break it up into functions.

Your labels variables does not contain anything, you can not chain very much in python. It's a design choice from the developers. So the variable of the labels is unnecessary.

This:

time.sleep(500/1000.0)

is fine kinda, but, it is equivalent to

time.sleep(1/2)

which is prettier.

There is a better way to find the current seconds, minutes and hours of current time,

def sec_to_time(sec):
 m, s = divmod(sec, 60)
 h, m = divmod(m, 60)
 return h, m, s

now you can:

h, m, s = sec_to_time(3727)

and h=1, m=2, s=7

Also the parameter you pass "Thread-1" is the same for all threads you start.


I'm into async right now, so I'll offer up a async solution. I tried to follow your structure but instead used only three Labels. And I think it's justifiable to use async in gui programs.

import asyncio
from time import strftime
from time import time
from tkinter import Tk
from tkinter import Button
from tkinter import Label
from tkinter import StringVar
from tkinter import TclError
from tkinter import FLAT
from tkinter.font import Font
class Timer:
 def __init__(self):
 self.loop = asyncio.get_event_loop()
 self.tk = Tk()
 self.tk.title("Elapsed Time")
 self.current_time = StringVar()
 self.timer_start = StringVar(value="00: 00: 00")
 self.timer_time = StringVar(value="00: 00: 00")
 font = Font(family="sans-serif", size=16)
 Button(self.tk, text="Start", command=self.activate_timer,
 font=font).grid(row=0, column=3)
 Button(self.tk, text="Stop", command=self.deactivate,
 font=font).grid(row=1, column=3)
 Label(self.tk, relief=FLAT, textvariable=self.timer_time,
 font=font).grid(row=0, column=0)
 Label(self.tk, relief=FLAT, textvariable=self.timer_start,
 font=font).grid(row=1, column=0)
 Label(self.tk, relief=FLAT, textvariable=self.current_time,
 font=font).grid(row=2, column=0)
 self.timer_on = False
 self.running = True
 self.sleep_time = 0.05
 def run_forever(self):
 self.loop.create_task(self.update_current_time())
 self.loop.create_task(self.run_tk())
 self.loop.run_forever()
 async def timer(self):
 self.timer_start.set(self.get_time())
 start = time()
 while self.timer_on:
 self.timer_time.set(self.sec_to_time(int(time()-start)))
 await asyncio.sleep(self.sleep_time)
 async def update_current_time(self):
 while self.running:
 self.current_time.set(self.get_time())
 await asyncio.sleep(self.sleep_time)
 async def run_tk(self):
 while self.running:
 try:
 self.tk.update()
 except TclError:
 self.running = self.timer_on = False
 await asyncio.sleep(self.sleep_time)
 asyncio.sleep(2)
 self.loop.stop()
 def activate_timer(self):
 self.timer_on = True
 self.loop.create_task(self.timer())
 def deactivate(self):
 self.timer_on = False
 @staticmethod
 def get_time():
 return strftime("%H: %M: %S")
 @staticmethod
 def sec_to_time(sec):
 m, s = divmod(sec, 60)
 return ": ".join([str(x) if x > 9 else "0"+str(x) for x
 in (*divmod(m, 60), s)])
def main():
 t = Timer()
 t.run_forever()
if __name__ == '__main__':
 main()

The last lines:

def main():
 t = Timer()
 t.run_forever()
if __name__ == '__main__':
 main()

give you the ability to import the timer class into another file. If you try to import your code into another file, you'll start your program and not complete the import.

answered Nov 14, 2016 at 8:33
\$\endgroup\$
4
  • \$\begingroup\$ Did you mean h, m = divmod(m, 60) instead of h, m = divmod(m, 69) ? \$\endgroup\$ Commented Nov 14, 2016 at 8:38
  • \$\begingroup\$ I do not understand this statement: " you can not chain very much in python. It's a design choice from the developers" could you please explain more in detail what it means? \$\endgroup\$ Commented Nov 14, 2016 at 14:58
  • \$\begingroup\$ I'm referring to "Method chaining" or more specific Label() .grid(row=0,column=0)", which returns None and is not "chaining", however, if gird did return exactly "self", you could "chain" more functions on the Lable().grid().somefunc()? Isn't that like php-jagong, ? in which these "chaining" is most usual. Anyways, python functions most commonly return "None" and not "self", and I think that is a "design choice from devs".. Didn't even think of it twice. I'm I wrong? \$\endgroup\$ Commented Nov 14, 2016 at 15:28
  • \$\begingroup\$ Thank you very much for the detailed code analysis and a finer version of code \$\endgroup\$ Commented Nov 16, 2016 at 0:51

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.