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()
\$\endgroup\$
6
  • \$\begingroup\$ Could you do it with 3 labels? \$\endgroup\$
    – Simon
    Nov 14 '16 at 1:09
  • \$\begingroup\$ @Simon Can you please elaborate your question? Labels are mainly used to display the value. \$\endgroup\$ Nov 14 '16 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\$
    – Simon
    Nov 14 '16 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\$ Nov 14 '16 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\$
    – Simon
    Nov 14 '16 at 1:38
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.

\$\endgroup\$
4
  • \$\begingroup\$ Did you mean h, m = divmod(m, 60) instead of h, m = divmod(m, 69) ? \$\endgroup\$ Nov 14 '16 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\$
    – Caridorc
    Nov 14 '16 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\$
    – Simon
    Nov 14 '16 at 15:28
  • \$\begingroup\$ Thank you very much for the detailed code analysis and a finer version of code \$\endgroup\$ Nov 16 '16 at 0:51

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.