AccuTimer().pbi, a replacement for AddWindowTimer()

Share your advanced PureBasic knowledge/code with the community.
User_Russian
Addict
Addict
Posts: 1549
Joined: Wed Nov 12, 2008 5:01 pm
Location: Russia

Re: AccuTimer().pbi, a replacement for AddWindowTimer()

Post by User_Russian »

The procedure AddAccuTimer(), there is an exit without releasing a mutex. This can create a problem.

Code: Select all

         If timeout < 20 Or RandomSpan < 0
            ProcedureReturn 0 ; error, parameters are below minimum
         EndIf
The procedures RemoveAccuTimer() and RemoveAllAccuTimers(), after the capture of a mutex in the cycle expected to complete the thread, but if this thread is waiting for a mutex, it will never end - the program will freeze.

Code: Select all

; AccuTimer.pbi
; by BasicallyPure
; version 1.1
; 7.21.2013
; license: Free
; OS = All
;
; <><><><><><><><><><><><><><><> AccuTimer instructions <><><><><><><><><><><><><><><>
; Description:
;  provides a timer similar To AddWindowTimer() but With more accuracy, and extra features
;  including 'pause', 'holdoff', and 'random intervals'.  AccuTimer timers are accurate
;  over time with system clock accuracy.  A built-in error correction removes accumulated
;  errors from timer events.
;
; Syntax: AddAccuTimer(nWindow, nTimer, Timeout, [Holdoff], [RandomSpan])
;
; Parameters:
;  nWindow | this is the number that can be retreived with EventWindow() after a #PB_Event_Timer event.
;
;  nTimer | The Timer parameter is a user defined number that identifies this timer.
;     This value will later be returned from EventTimer() when a #PB_Event_Timer is received.
;     It can also be used To remove the timer again with the RemoveAccuTimer() function.
;
;  Timeout | This specifies the amount of time in milliseconds between #PB_Event_Timer events.
;     The value of timeout is required to be >= 20 milliseconds.
;
;  Holdoff | This optional parameter produces an extra delay in milliseconds before the first timer event
;     from this timer is produced.  This can be used to produce a phase offset between two Accutimers that
;     have the same timeout value.
;
;  RandomSpan | This optional parameter will cause the timer to produce #PB_Event_Timer events that have
;     random time intervals where timeout is the minimum interval and timeout + randomSpan is the maximum
;     interval.
;
; <><><> Support procedures: <><><>
;
;  PauseAccuTimer(nTimer, state)
;     This procedure will pause or resume a timer's operation.
;     nTimer | the timer number to pause or resume.
;     state  | 1 = pause, 0 = resume
;
;  ReviseAccuTimer(nTimer.i, timeout.i, holdOff.i = 0, RandomSpan.i = 0)
;     This procedure allows changing a timer's parameters after it has been created with AddAccuTimer().
;     The parameters are the same as for AddAccuTimer().
;
;  RemoveAccuTimer(nTimer)
;     Removes the specified AccuTimer.
;     nTimer is the same value that was used in AddAccuTimer() To create the timer.
;     There will be no further events received from this timer. 
;     If all of the AccuTimers have been removed the AccuTimer thread will end.
;
;  RemoveAllAccuTimers()
;     Removes all AccuTimers created with AddAccuTimer()
;     The AccuTimer thread will end.
;
; <><><><><><><><><><><><><><><><> End instructions <><><><><><><><><><><><><><><><>

CompilerIf Not #PB_Compiler_Thread
   MessageRequester("WARNING!", "Set compiler options: 'Create threadsafe executable'")
   End
CompilerEndIf

; this code is compatable with EnableExplicit
; EnableExplicit

Global.i AccuTimerThreadID
Global.i StopAccuTimerThread

Structure timerType
   winNum.i  ; window number returned by EventWindow()
   timNum.i  ; unique number to identify each AccuTimer
   rtMax.i   ; upper time limit for random timer, 0 for fixed timer
   rtMin.i   ; lower time limit for random timer, 0 for fixed timer
   timeout.i ; time between #PB_Event_Timer events in milliseconds
   paused.i  ; flag to pause timer, 1 = paused, 0 = run
   start.i   ; set equal to ElapsedMilliseconds() when initalized
EndStructure

Global NewMap AccuTimer.timerType()
Global Mutex1 = CreateMutex()

Procedure AccuTimer_Thread(unused.i)
   ; Purpose generate #PB_Event_Timer events for each structured map element of AccuTimer().
   ; Multiple timers may run simultaneously.
   
   Protected.i now, elapsed, err
   StopAccuTimerThread = #False
   
   Repeat
      now = ElapsedMilliseconds()
      LockMutex(Mutex1)
         ForEach AccuTimer()
            With AccuTimer()
               If Not \paused
                  elapsed = now - \start
                  If elapsed >= \timeout
                     err = elapsed - \timeout
                     \start = now - err
                     PostEvent(#PB_Event_Timer, \winNum, \timNum)
                     If \rtMax : \timeout = Random(\rtMax, \rtMin) : EndIf
                  EndIf
               EndIf
            EndWith
         Next
      UnlockMutex(Mutex1)
      
      Delay(10) ; arbitrary delay sets lower limit of timeout
   Until StopAccuTimerThread
EndProcedure

Procedure AddAccuTimer(nWindow.i, nTimer.i, timeout.i, holdOff.i = 0, RandomSpan.i = 0)
   ; Purpose: create a new AccuTimer with the specified parameters. #PB_Event_Timer events
   ;          will be received by WindowEvent() or WaitWindowEvent().
   ;
   ; #Window | this is the number that is returned by EventWindow() after a #PB_Event_Timer event.
   ;
   ; nTimer  | The user defined number used to identify this timer.
   ;
   ; timeout | This specifies the amount of time in milliseconds between #PB_Event_Timer events.
   ;           The value of timeout is required to be >= 20 milliseconds.
   ;
   ; holdoff | This optional parameter produces an extra delay in milliseconds before the first timer event
   ;
   ; RandomSpan | This optional parameter will cause the timer To produce #PB_Event_Timer events that have
   ;              random time intervals where 'timeout' sets the minimum interval and 'timeout + RandomSpan'
   ;              sets the maximum interval.
   
   Protected result.i = #False
   
   If Not IsWindow(nWindow)
      ProcedureReturn result ; error, window is not valid
   EndIf
   
   LockMutex(Mutex1)
      If FindMapElement(AccuTimer(), Str(nTimer)) = 0
         If IsThread(AccuTimerThreadID) = #False
            AccuTimerThreadID = CreateThread(@AccuTimer_Thread(), #NUL)
         EndIf
         
         If timeout < 20 Or RandomSpan < 0
   UnlockMutex(Mutex1)
            ProcedureReturn 0 ; error, parameters are below minimum
         EndIf
         
         With AccuTimer(Str(nTimer)) ; this creates a new map element
            \winNum = nWindow
            \timNum = nTimer
            If RandomSpan
               \rtMax = timeout + RandomSpan
               \rtMin = timeout
               \timeout = Random(\rtMax, \rtMin)
            Else
               \timeout = timeout
            EndIf
            \paused = #False
            \start  = ElapsedMilliseconds() + holdOff
         EndWith
         
         result = #True
      EndIf
   UnlockMutex(Mutex1)
   
   ProcedureReturn result
EndProcedure

Procedure PauseAccuTimer(nTimer.i, state.i)
   ; suspend/enable timer events of the specified timer
   ; state = 0 (enable), state = 1 (suspend)
   
   Protected nt$ = Str(nTimer), Result = #False
   If state <> 0 : state = 1 : EndIf
   
   LockMutex(Mutex1)
      If FindMapElement(AccuTimer(), nt$) ; check for valid timer number
         With AccuTimer(nt$)
            \paused = state
            If state = 0 : \start = ElapsedMilliseconds() : EndIf
          EndWith
          Result = #True
      EndIf
   UnlockMutex(Mutex1)
   
   ProcedureReturn Result
EndProcedure

Procedure ReviseAccuTimer(nTimer.i, timeout.i, holdOff.i = 0, RandomSpan.i = 0)
   ; purpose: change the settings of a timer that has already been created with AddAccuTimer().
   
   Protected nt$ = Str(nTimer), result.i = #False
   
   LockMutex(Mutex1)
      ; check for valid timer number
      If FindMapElement(AccuTimer(), nt$) And timeout >= 20
         
         With AccuTimer(nt$)
            If RandomSpan
               \rtMax = timeout + RandomSpan
               \rtMin = timeout
               \timeout = Random(\rtMax, \rtMin)
            Else
               \rtMax = 0
               \rtMin = 0
               \timeout = timeout
            EndIf
            \start  = ElapsedMilliseconds() + holdOff
         EndWith
         
         result = #True
      EndIf
   UnlockMutex(Mutex1)
   
   ProcedureReturn result
EndProcedure

Procedure RemoveAccuTimer(nTimer.i)
   ; Purpose: Removes the specified AccuTimer.
   ; nTimer has To be the same value that was used in AddAccuTimer() To create the timer.
   ; There will be no further events received from this timer. 

   Protected nt$ = Str(nTimer), result.i = #False
   
   LockMutex(Mutex1)
      If FindMapElement(AccuTimer(), nt$) ; check for valid timer number
        DeleteMapElement(AccuTimer(),nt$)
        
   UnlockMutex(Mutex1)
        
         If MapSize(AccuTimer()) = 0
           StopAccuTimerThread = #True ; no AccuTimers exist so stop the thread
           WaitThread(AccuTimerThreadID, 2000)
           If IsThread(AccuTimerThreadID)
             KillThread(AccuTimerThreadID)
           EndIf
;             While IsThread(AccuTimerThreadID) ; wait for the thread to end
;                Delay(1)
;             Wend
         EndIf
         
         result = #True
         ProcedureReturn result
      EndIf
   UnlockMutex(Mutex1)
   
   ProcedureReturn result
EndProcedure

Procedure RemoveAllAccuTimers()
   ; Purpose: removes all AccuTimers created with AddAccuTimer()
   Protected result.i = #False
   
   LockMutex(Mutex1)
      If MapSize(AccuTimer())
     
         ForEach AccuTimer()
            DeleteMapElement(AccuTimer())
         Next
       
   UnlockMutex(Mutex1)
        StopAccuTimerThread = #True
        
        WaitThread(AccuTimerThreadID, 2000)
        If IsThread(AccuTimerThreadID)
          KillThread(AccuTimerThreadID)
        EndIf
     
;          While IsThread(AccuTimerThreadID) ; wait for the thread to end
;             Delay(1)
;          Wend
         
         result = #True
         ProcedureReturn result
      EndIf
   UnlockMutex(Mutex1)
   
   ProcedureReturn result
EndProcedure


; ******************************************************************* ;
;- DEMONSTRATION -
; you can leave this in the include file

CompilerIf #PB_Compiler_IsMainFile
   #WinMain = 0
   #WinAux1 = 1
   
   Enumeration
      #strGad_01
      #strGad_02
      #strGad_03
      #btnRevise
      #btnPause
      #btnRemove
      #btnRemoveAll
      #ProgBar
   EndEnumeration
   
   Enumeration
      #AccuTimer_01
      #AccuTimer_02
      #AccuTimer_03
      #WindowTimer_01
   EndEnumeration
   
   Define text.s = "Comparison AccuTimer vs WindowTimer"
   If OpenWindow(#WinMain,020,300,280,250,text) = 0 : End : EndIf
   text = "bar movement is triggered by AccuTimer's random interval event option"
   If OpenWindow(#WinAux1,330,300,500,100,text,#PB_Window_TitleBar) = 0 : End : EndIf
   SetActiveWindow(#WinMain)
   
   Define Courier_ID = LoadFont(0,"Courier New",28,#PB_Font_Bold)
   
   UseGadgetList(WindowID(#WinMain))
      TextGadget(#PB_Any,10,3,250,20,"PB Date() function as reference")
      StringGadget(#strGad_01,10,20,250,50,"00:00:00")
      SetGadgetFont(#strGad_01,Courier_ID)
      TextGadget(#PB_Any,10,83,250,20,"variable incremented with AccuTimer")
      StringGadget(#strGad_02,10,100,250,50,"00:00:00")
      SetGadgetFont(#strGad_02,Courier_ID)
      TextGadget(#PB_Any,10,163,250,20,"variable incremented with PB WindowTimer")
      StringGadget(#strGad_03,10,180,250,50,"00:00:00")
      SetGadgetFont(#strGad_03,Courier_ID)
   
   UseGadgetList(WindowID(#WinAux1))
      ButtonGadget(#btnPause, 5, 70, 80, 25, "Pause bar", #PB_Button_Toggle)
      ProgressBarGadget(#ProgBar, 0, 0, 500, 60, 0, 500)
      ButtonGadget(#btnRevise, 90, 70, 130, 25, "Revise bar timer", #PB_Button_Toggle)
      ButtonGadget(#btnRemove, 225, 70, 130, 25, "Remove AccuTimer_02")
      ButtonGadget(#btnRemoveAll, 360, 70, 130, 25, "Remove all AccuTimers")
   
   ; ******* Initalize the timers ***************
   ;
   AddAccuTimer(#WinMain, #AccuTimer_01, 1000) ; <-- event every second
   AddAccuTimer(#WinMain, #AccuTimer_02, 100) ; <-- event every 100 mS
   AddAccuTimer(#WinAux1, #AccuTimer_03, 200, 0, 2000) ;<-- random event intervals 200 to 200 + 2000 mS
   ;
   AddWindowTimer(#WinMain, #WindowTimer_01, 100) ; <-- event approximately every 100 mS
   ;
   ; ********************************************
   
   Define.i AccuCount ; <-- variable to be incremented by AccuTimer event
   Define.i WindowTimerCount ; <-- variable to be incremented by WindowTimer event
   
   ; initalize both reference variables to the current time
   Define ACref.i = Date() ; for AccuTimer
   Define WTref.i = Date() ; for WindowTimer
   
   Define progress.i = 10
   Dim caption.s(1) : caption(0) = "Pause bar" : caption(1) = "Resume"
   
   Repeat
      Select WaitWindowEvent()
         Case #PB_Event_CloseWindow : End
            
         Case #PB_Event_Timer; AccuTimer produces events just like the WindowTimer
            
            Select EventTimer()
               Case #AccuTimer_01 ; update the display of the Date() function
                  SetGadgetText(#strGad_01, FormatDate("%hh:%ii:%ss", Date()))
                  
               Case #AccuTimer_02 ; update the display of the AccuTimer counter
                  AccuCount + 1
                  SetGadgetText(#strGad_02, FormatDate("%hh:%ii:%ss", ACref + AccuCount/10)+"."+Str(AccuCount%10))
                  
               Case #WindowTimer_01 ; update the display of the WindowTimer counter
                  WindowTimerCount + 1
                  SetGadgetText(#strGad_03, FormatDate("%hh:%ii:%ss", WTref + WindowTimerCount/10)+"."+Str(WindowTimerCount%10))
                  
               Case #AccuTimer_03 ; move the bar at the randomly constrained time interval
                  SetGadgetState(#ProgBar, progress) : progress + 10
                  If progress > 500 : progress = 0 : EndIf
            EndSelect
            
         Case #PB_Event_Gadget
            
            Select EventGadget()
               Case #btnPause ; demonstrate PauseAccuTimer()
                  PauseAccuTimer(#AccuTimer_03, GetGadgetState(#btnPause))
                  SetGadgetText(#btnPause, caption(GetGadgetState(#btnPause)))
                  
               Case #btnRemove ; demonstrate RemoveAccuTimer()
                  RemoveAccuTimer(#AccuTimer_02)
                  DisableGadget(#btnRemove, #True)
                  
               Case #btnRemoveAll ; demonstrate RemoveAllAccuTimers()
                  RemoveAllAccuTimers()
                  DisableGadget(#btnRemove, #True)
                  DisableGadget(#btnPause, #True)
                  DisableGadget(#btnRemoveAll, #True)
                  
               Case #btnRevise ; demonstrate ReviseAccuTimer()
                  If GetGadgetState(#btnRevise)
                     ReviseAccuTimer(#AccuTimer_03, 50, 0, 250)
                  Else
                     ReviseAccuTimer(#AccuTimer_03, 200, 0, 2000)
                   EndIf
            EndSelect
      EndSelect
   ForEver
   
CompilerEndIf
Joris
Addict
Addict
Posts: 890
Joined: Fri Oct 16, 2009 10:12 am
Location: BE

Re: AccuTimer().pbi, a replacement for AddWindowTimer()

Post by Joris »

@BasicallyPure you're talking about interruptions "if you hold the mouse button down on the title bar of a window some of the events will be missed"...
Did you use ThreadPriority(Thread, 32) setting when experiencing the 12 milliseconds interruptions ?
I'm not that experienced with threads but it shouldn't be the case then.
Yeah I know, but keep in mind ... Leonardo da Vinci was also an autodidact.
User avatar
BasicallyPure
Enthusiast
Enthusiast
Posts: 539
Joined: Thu Mar 24, 2011 12:40 am
Location: Iowa, USA

Re: AccuTimer().pbi, a replacement for AddWindowTimer()

Post by BasicallyPure »

@User_Russian,
Thank you for finding the problems in my code.
I have updated the code in my first post.
Please inspect for correctness as I have made some small changes from the code you posted.
Joris wrote:Did you use ThreadPriority(Thread, 32) setting when experiencing the 12 milliseconds interruptions ?
No, the thread priority was the default value.

BP
BasicallyPure
Until you know everything you know nothing, all you have is what you believe.
User_Russian
Addict
Addict
Posts: 1549
Joined: Wed Nov 12, 2008 5:01 pm
Location: Russia

Re: AccuTimer().pbi, a replacement for AddWindowTimer()

Post by User_Russian »

BasicallyPure wrote:Please inspect for correctness as I have made some small changes from the code you posted.
Should work fine.
Post Reply