Page 1 of 1

YAP - Yet Another Progressbar

Posted: Sat Apr 16, 2011 6:02 pm
by pTb
Dunno if You've seen to many of these routines already, but here's a procedure for initing, showing and ending a progress bar. Useful for heavy duty loops that perhaps already is optimized but still takes more then a few tens of a second to complete.

This procedure is fairly intelligent, adaptive, optimized and easy to use. Fairly. You can surely find improvements to do with it. ;)

So suggestions, comments, improvements, questions will probably be read and perhaps processed, commented, validated and answered.

Well, enough scribbling, here's the code:

Code: Select all



; quick reference:
	; ProgressInfo(#ProgressInit, topValue)        ;before loop - topValue is where the job will end
	; ProgressInfo(#ProgressShow, partOfTopValue)  ;inside loop - partOfTopValue is where the job is
	; ProgressInfo(#ProgressClose)                 ;after loop



;- Window Constants
;
Enumeration
  #wProgress
EndEnumeration

;- Gadget Constants
;
Enumeration  
  #ProgressBar	
EndEnumeration

;- Action Constants
;
Enumeration	
	#ProgressInit
	#ProgressShow
	#ProgressClose
EndEnumeration


Procedure ProgressInfo(action.l, value.q = -1)
	
	; A progress bar is shown if something takes more than a defined time to complete and less than 
	; a defined part of the job is done. If the progress has past that part, no progress will show up.

	; The progress bar is updateted depending on how fast the progress is as well as when the 
	; procedure is called. The slowest of the two decides the update interval.
	
	; A deeper explanation of the last sentence:
	; If a job has been completed 4% when the window is opened (all criteria satisfied), the update interval
	; will be about 2 * #minInterval ms even if the calls only have 2 ms in delay.
	
	; How to use:
	; init: ProgressInfo(#ProgressInit, topValue)
	; show: ProgressInfo(#ProgressShow, partOfTopValue)
	; quit: ProgressInfo(#ProgressClose)
	;
	; All three should be used. #ProgressShow is used inside the loop that should be monitored.
	; topValue can preferably be the loops stop value and partOfTopValue the counting up (index) in the loop.
	; Usually the outer loop is the one to monitor.
	;
	; Atm #PB_ProgressBar_Smooth is used to show how smooth/jaggy the bar can be, feel free change its looks.
	
	;useful constants
	#timeBeforeWindowOpening = 150 ;(unit: ms) 50 to 500 seems to be reasonable.
	#maxProgressToOpenWindow = 6   ;(unit: parts) 4 means 1/4 (25%). If the job has progressed more than 
	                               ;              this, no progress bar will show up.
	#minInterval = 15              ; minimum time between each update of the progress bar
	
	; There will be a delay of #timeBeforeWindowOpening before testing if the progress is 1 / #maxProgressToOpenWindow
	; of the upper value
	; If the progress is less, the progress bar will be shown.
	; If the progress is more, no progress bar will be shown.
	; A loop that takes up to #timeBeforeWindowOpening * #maxProgressToOpenWindow milliseconds will not 
	; show a progress bar. Thats why the product of theese two is important. That's the max time before the
	; user probably would get worried that the computer didn't respond well to his/her last command. :)
	
	Static progressTimer.l, upperValue.l, windowOpen.l, totalTimer.l, interval.q, divider.q
	Global loopSkip.l

	Select action
	
		Case #ProgressShow
;       ; switch this error check on if you are uncertain of your code - switch it of to save a few cycles
; 			If value < 0
; 				MessageRequester("ProgressInfo", "value must be set at Show of progress")
; 				End
; 			EndIf
			If windowOpen = #True And ElapsedMilliseconds() - progressTimer > interval			
  			value / divider
				SetGadgetState(#ProgressBar, value)
				progressTimer = ElapsedMilliseconds()
  		ElseIf ElapsedMilliseconds() > progressTimer
			  If windowOpen = #False
  				If value * #maxProgressToOpenWindow < upperValue
  				  upperValue / divider
  		 			If OpenWindow(#wProgress, 496, 300, 290, 51, "Busy",  #PB_Window_TitleBar | #PB_Window_BorderLess | #PB_Window_ScreenCentered ) ; "Arbetar"
  		 				StickyWindow(#wProgress, #True)
  		 				ProgressBarGadget(#ProgressBar, 10, 10, 270, 30, 0, upperValue, #PB_ProgressBar_Smooth)
  		 				windowOpen = #True
  		 				
  		 				; räkna ut ett lagom intervall för uppdateringen
  		 				If value = 0
  		 					interval = #timeBeforeWindowOpening * 80 / 100
  		 				Else
  		 				  value / divider
  		 					interval = upperValue / value * 2
  		 					If interval < #minInterval
  		 						interval = #minInterval
  		 					EndIf
  		 					If interval > #timeBeforeWindowOpening * 80 / 100
  		 						interval = #timeBeforeWindowOpening * 80 / 100
  		 					EndIf
  		 				EndIf
   		 				
  		 			EndIf	
  				EndIf
  			EndIf
			EndIf


		Case #ProgressInit
			If value <= 0
				MessageRequester("ProgressInfo", "Top value must be sent as argument at init")
				End
			EndIf
			If #minInterval * 1.25 > #timeBeforeWindowOpening
			  MessageRequester("ProgressInfo", "#minInterval must be 25% greater than #timeBeforeWindowOpening")
			  End
			EndIf
			; set divider for great values (for not messing up the gadget)
			If value > 10000
				divider = value / 10000
			Else
				divider = 1
			EndIf
			progressTimer = ElapsedMilliseconds() + #timeBeforeWindowOpening
			totalTimer = progressTimer
			upperValue = value 
			
      loopSkip = Log10(upperValue / 10000) / Log10(2) + 0.5
      If loopSkip < 0: loopSkip = 0: EndIf  
      loopSkip = Pow(2, loopSkip) - 1
  
			
		
		Case #ProgressClose
			If windowOpen = #True
				FreeGadget(#ProgressBar)
				CloseWindow(#wProgress)
				upperValue = 0
				progressTimer = 0
				windowOpen = #False
			EndIf
			
	EndSelect
		
EndProcedure

Macro ProgressInfo_laced(action, value)

  If value & loopSkip = 0
    ProgressInfo(action, value)
  EndIf
  
EndMacro


; test with ordinary implementation
Debug "Ordinary method:"

outerLoopTop = 1
totalLoopSize = 20000000
loopMultiplier = 5

While totalLoopSize / outerLoopTop / loopMultiplier > 0

  outerLoopTop = totalLoopSize / (totalLoopSize / outerLoopTop / loopMultiplier)
  timeTracker = ElapsedMilliseconds()
  
  ProgressInfo(#ProgressInit, outerLoopTop)
  
  For i = 1 To outerLoopTop
    For j = 1 To totalLoopSize / outerLoopTop
    Next
    ProgressInfo(#ProgressShow, i)
  Next
  
  ProgressInfo(#ProgressClose)
  Debug "innerLoop: "+Str(totalLoopSize/outerLoopTop)+", outerLoop:"+Str(outerLoopTop)+", Time(ms):"+Str(ElapsedMilliseconds() - timeTracker)

Wend 


; for use with tight inner loops, but more complex
Debug ""
Debug "Method for tight inner loops:"

outerLoopTop = 1
totalLoopSize = 20000000
loopMultiplier = 5

While totalLoopSize / outerLoopTop / loopMultiplier > 0

  outerLoopTop = totalLoopSize / (totalLoopSize / outerLoopTop / loopMultiplier)
  timeTracker = ElapsedMilliseconds()
  
  ProgressInfo(#ProgressInit, outerLoopTop)
  
  For i = 1 To outerLoopTop
    For j = 1 To totalLoopSize / outerLoopTop
    Next
    ProgressInfo_laced(#ProgressShow, i)
  Next
  
  ProgressInfo(#ProgressClose)
  Debug "innerLoop: "+Str(totalLoopSize/outerLoopTop)+", outerLoop:"+Str(outerLoopTop)+", Time(ms):"+Str(ElapsedMilliseconds() - timeTracker)

Wend


; only for reference - no progress info is used here
Debug ""
Debug "Reference run (no progress meter):"

outerLoopTop = 1
totalLoopSize = 20000000
loopMultiplier = 5

While totalLoopSize / outerLoopTop / loopMultiplier > 0
  outerLoopTop = totalLoopSize / (totalLoopSize / outerLoopTop / loopMultiplier)
  timeTracker = ElapsedMilliseconds()
  For i = 1 To outerLoopTop
    For j = 1 To totalLoopSize / outerLoopTop
    Next
  Next
  Debug "innerLoop: "+Str(totalLoopSize/outerLoopTop)+", outerLoop:"+Str(outerLoopTop)+", Time(ms):"+Str(ElapsedMilliseconds() - timeTracker)
Wend

Re: YAP - Yet Another Progressbar

Posted: Sat Apr 16, 2011 9:03 pm
by Nituvious
That's really good! Thank you for sharing!

Re: YAP - Yet Another Progressbar

Posted: Sun Apr 17, 2011 1:26 am
by Demivec
The time results aren't reliable if the debugger is used.

Try this version instead, without debugger:

Code: Select all

DisableDebugger
; quick reference:
   ; ProgressInfo(#ProgressInit, topValue)        ;before loop - topValue is where the job will end
   ; ProgressInfo(#ProgressShow, partOfTopValue)  ;inside loop - partOfTopValue is where the job is
   ; ProgressInfo(#ProgressClose)                 ;after loop



;- Window Constants
;
Enumeration
  #wProgress
  #wEditor
EndEnumeration

;- Gadget Constants
;
Enumeration 
  #ProgressBar   
  #Editor
EndEnumeration

;- Action Constants
;
Enumeration   
   #ProgressInit
   #ProgressShow
   #ProgressClose
EndEnumeration


Procedure ProgressInfo(action.l, Value.q = -1)
   
   ; A progress bar is shown if something takes more than a defined time to complete and less than
   ; a defined part of the job is done. If the progress has past that part, no progress will show up.

   ; The progress bar is updateted depending on how fast the progress is as well as when the
   ; procedure is called. The slowest of the two decides the update interval.
   
   ; A deeper explanation of the last sentence:
   ; If a job has been completed 4% when the window is opened (all criteria satisfied), the update interval
   ; will be about 2 * #minInterval ms even if the calls only have 2 ms in delay.
   
   ; How to use:
   ; init: ProgressInfo(#ProgressInit, topValue)
   ; show: ProgressInfo(#ProgressShow, partOfTopValue)
   ; quit: ProgressInfo(#ProgressClose)
   ;
   ; All three should be used. #ProgressShow is used inside the loop that should be monitored.
   ; topValue can preferably be the loops stop value and partOfTopValue the counting up (index) in the loop.
   ; Usually the outer loop is the one to monitor.
   ;
   ; Atm #PB_ProgressBar_Smooth is used to show how smooth/jaggy the bar can be, feel free change its looks.
   
   ;useful constants
   #timeBeforeWindowOpening = 150 ;(unit: ms) 50 to 500 seems to be reasonable.
   #maxProgressToOpenWindow = 6   ;(unit: parts) 4 means 1/4 (25%). If the job has progressed more than
                                  ;              this, no progress bar will show up.
   #minInterval = 15              ; minimum time between each update of the progress bar
   
   ; There will be a delay of #timeBeforeWindowOpening before testing if the progress is 1 / #maxProgressToOpenWindow
   ; of the upper value
   ; If the progress is less, the progress bar will be shown.
   ; If the progress is more, no progress bar will be shown.
   ; A loop that takes up to #timeBeforeWindowOpening * #maxProgressToOpenWindow milliseconds will not
   ; show a progress bar. Thats why the product of theese two is important. That's the max time before the
   ; user probably would get worried that the computer didn't respond well to his/her last command. :)
   
   Static progressTimer.l, upperValue.l, windowOpen.l, totalTimer.l, interval.q, divider.q
   Global loopSkip.l

   Select action
   
      Case #ProgressShow
;       ; switch this error check on if you are uncertain of your code - switch it of to save a few cycles
;          If value < 0
;             MessageRequester("ProgressInfo", "value must be set at Show of progress")
;             End
;          EndIf
         If windowOpen = #True And ElapsedMilliseconds() - progressTimer > interval         
           Value / divider
            SetGadgetState(#ProgressBar, Value)
            progressTimer = ElapsedMilliseconds()
        ElseIf ElapsedMilliseconds() > progressTimer
           If windowOpen = #False
              If Value * #maxProgressToOpenWindow < upperValue
                upperValue / divider
                  If OpenWindow(#wProgress, 496, 300, 290, 51, "Busy",  #PB_Window_TitleBar | #PB_Window_BorderLess | #PB_Window_ScreenCentered ) ; "Arbetar"
                     StickyWindow(#wProgress, #True)
                     ProgressBarGadget(#ProgressBar, 10, 10, 270, 30, 0, upperValue, #PB_ProgressBar_Smooth)
                     windowOpen = #True
                     
                     ; räkna ut ett lagom intervall för uppdateringen
                     If Value = 0
                        interval = #timeBeforeWindowOpening * 80 / 100
                     Else
                       Value / divider
                        interval = upperValue / Value * 2
                        If interval < #minInterval
                           interval = #minInterval
                        EndIf
                        If interval > #timeBeforeWindowOpening * 80 / 100
                           interval = #timeBeforeWindowOpening * 80 / 100
                        EndIf
                     EndIf
                      
                  EndIf   
              EndIf
           EndIf
         EndIf


      Case #ProgressInit
         If Value <= 0
            MessageRequester("ProgressInfo", "Top value must be sent as argument at init")
            End
         EndIf
         If #minInterval * 1.25 > #timeBeforeWindowOpening
           MessageRequester("ProgressInfo", "#minInterval must be 25% greater than #timeBeforeWindowOpening")
           End
         EndIf
         ; set divider for great values (for not messing up the gadget)
         If Value > 10000
            divider = Value / 10000
         Else
            divider = 1
         EndIf
         progressTimer = ElapsedMilliseconds() + #timeBeforeWindowOpening
         totalTimer = progressTimer
         upperValue = Value
         
      loopSkip = Log10(upperValue / 10000) / Log10(2) + 0.5
      If loopSkip < 0: loopSkip = 0: EndIf 
      loopSkip = Pow(2, loopSkip) - 1
 
         
      
      Case #ProgressClose
         If windowOpen = #True
            FreeGadget(#ProgressBar)
            CloseWindow(#wProgress)
            upperValue = 0
            progressTimer = 0
            windowOpen = #False
         EndIf
         
   EndSelect
      
EndProcedure

Macro ProgressInfo_laced(action, Value)

  If Value & loopSkip = 0
    ProgressInfo(action, Value)
  EndIf
 
EndMacro

Procedure updateOutput(text.s = "", gadget = #Editor)
  AddGadgetItem(gadget, -1, text)
  Repeat: Until WindowEvent() = 0
EndProcedure

If Not OpenWindow(#wEditor, 0, 0, 400, 500, "Results", #PB_Window_SystemMenu) Or Not EditorGadget(#Editor, 0, 0, 400, 500)
  MessageRequester("Error", "Failed to setup output window and output gadget.")
  End
EndIf

; test with ordinary implementation
;Debug "Ordinary method:"
updateOutput("Ordinary method:")

outerLoopTop = 1
totalLoopSize = 20000000
loopMultiplier = 5

While totalLoopSize / outerLoopTop / loopMultiplier > 0

  outerLoopTop = totalLoopSize / (totalLoopSize / outerLoopTop / loopMultiplier)
  timeTracker = ElapsedMilliseconds()
 
  ProgressInfo(#ProgressInit, outerLoopTop)
 
  For i = 1 To outerLoopTop
    For j = 1 To totalLoopSize / outerLoopTop
    Next
    ProgressInfo(#ProgressShow, i)
  Next
 
  ProgressInfo(#ProgressClose)
  ;Debug "innerLoop: "+Str(totalLoopSize/outerLoopTop)+", outerLoop:"+Str(outerLoopTop)+", Time(ms):"+Str(ElapsedMilliseconds() - timeTracker)
  updateOutput("innerLoop: "+Str(totalLoopSize/outerLoopTop)+", outerLoop:"+Str(outerLoopTop)+", Time(ms):"+Str(ElapsedMilliseconds() - timeTracker))  

Wend


; for use with tight inner loops, but more complex
;Debug ""
;Debug "Method for tight inner loops:"
updateOutput("")
updateOutput("Method for tight inner loops:")

outerLoopTop = 1
totalLoopSize = 20000000
loopMultiplier = 5

While totalLoopSize / outerLoopTop / loopMultiplier > 0

  outerLoopTop = totalLoopSize / (totalLoopSize / outerLoopTop / loopMultiplier)
  timeTracker = ElapsedMilliseconds()
 
  ProgressInfo(#ProgressInit, outerLoopTop)
 
  For i = 1 To outerLoopTop
    For j = 1 To totalLoopSize / outerLoopTop
    Next
    ProgressInfo_laced(#ProgressShow, i)
  Next
 
  ProgressInfo(#ProgressClose)
  ;Debug "innerLoop: "+Str(totalLoopSize/outerLoopTop)+", outerLoop:"+Str(outerLoopTop)+", Time(ms):"+Str(ElapsedMilliseconds() - timeTracker)
  updateOutput("innerLoop: "+Str(totalLoopSize/outerLoopTop)+", outerLoop:"+Str(outerLoopTop)+", Time(ms):"+Str(ElapsedMilliseconds() - timeTracker))  
  
Wend


; only for reference - no progress info is used here
;Debug ""
;Debug "Reference run (no progress meter):"
updateOutput("")
updateOutput("Reference run (no progress meter):")


outerLoopTop = 1
totalLoopSize = 20000000
loopMultiplier = 5

While totalLoopSize / outerLoopTop / loopMultiplier > 0
  outerLoopTop = totalLoopSize / (totalLoopSize / outerLoopTop / loopMultiplier)
  timeTracker = ElapsedMilliseconds()
  For i = 1 To outerLoopTop
    For j = 1 To totalLoopSize / outerLoopTop
    Next
  Next
  ;Debug "innerLoop: "+Str(totalLoopSize/outerLoopTop)+", outerLoop:"+Str(outerLoopTop)+", Time(ms):"+Str(ElapsedMilliseconds() - timeTracker)
  updateOutput("innerLoop: "+Str(totalLoopSize/outerLoopTop)+", outerLoop:"+Str(outerLoopTop)+", Time(ms):"+Str(ElapsedMilliseconds() - timeTracker))  
  
Wend

Repeat: Until WaitWindowEvent() = #PB_Event_CloseWindow
My test runs, using the above code, don't indicate any significant differences between the two methods.

@Edit: Corrected above code only for the alternate display method (instead of debugger). It still has inherent flaws that were in the original unmodified code.

Re: YAP - Yet Another Progressbar

Posted: Sun Apr 17, 2011 9:58 am
by pTb
@Nituvious: np!

@Demivec:

A good tip, thanx!

The difference between the two methods is shown when using very tight inner loops - when 'innerloop' is very low in this case. Eg. when the inner loop doesn't do very much and ProgressInfo is called very often. Usually (or always in my case), I haven't needed the macro. The procedure is probably sufficient in most cases. I actually added it before putting it here. ;)

You also have to increase the total number of iterations so the time for each test is at least 1000 ms to be able to see the difference fairly reliably, since Elapsedmilliseconds() is not that sharp in giving exact time, I think.

Another thing, I had to change the gadget number of your editor to be able to see it when the progress gadget appeared.

Simple but crude, change to this:

Code: Select all

#Editor = 1 ; anything but zero since the progress bar is using that
; to do it right, it should be put in the gadget enumeration in the beginning of the code
; and every call to updateOutput() should have the extra argument [, #Editor]
Procedure updateOutput(text.s = "", gadget = #Editor)
  AddGadgetItem(gadget, -1, text)
  Repeat: Until WindowEvent() = 0
EndProcedure
But when I got it working it showed a much lower difference between the two methods (and between not using a progress info at all). So, I thank you for the tip of skipping the debugger. :D

So, it seems the YAP is very transparent. And that's where my goal was.

What's very good with this procedure, is that it will never show up on a fast enough computer, but show up and adapt to a slower computer. Anyway - *should*. ;)

Re: YAP - Yet Another Progressbar

Posted: Sat Jul 16, 2016 2:31 pm
by pTb
I have finally come to posting updated code for my YAP.

The first one is about the same code as the first posted in this thread, but now it's a module instead, making it totally isolated from the code it's used in.

The other one is also a module but this one uses a dedicated thread for updating the progress - making the problem with tight inner loops totally transparent.

One problem/warning with this one though, it uses a window inside the thread. When trying to debug the modules code - it will fail, telling you that you can't use windows in threads...

... but I WANTED to use a window in a thread, so I turned off debugging and it worked. But turn on *threadsafe*, or it can break havoc.

If you try these routines, tell me if they work.

Comments can be found in the main routines.

Feel free to use this code as You wish.

Here are my results from YAP V2:

Code: Select all

Ordinary method:
innerLoop: 16000000, outerLoop:25, Time(ms):1359
innerLoop: 3200000, outerLoop:125, Time(ms):1344
innerLoop: 640000, outerLoop:625, Time(ms):1340
innerLoop: 128000, outerLoop:3125, Time(ms):1331
innerLoop: 25600, outerLoop:15625, Time(ms):1338
innerLoop: 5120, outerLoop:78125, Time(ms):1339
innerLoop: 1024, outerLoop:390625, Time(ms):1348
innerLoop: 204, outerLoop:1960784, Time(ms):1401
innerLoop: 40, outerLoop:10000000, Time(ms):1553
innerLoop: 8, outerLoop:50000000, Time(ms):2396
innerLoop: 1, outerLoop:400000000, Time(ms):9747

Method for tight inner loops:
innerLoop: 16000000, outerLoop:25, Time(ms):1101
innerLoop: 3200000, outerLoop:125, Time(ms):1110
innerLoop: 640000, outerLoop:625, Time(ms):1117
innerLoop: 128000, outerLoop:3125, Time(ms):1115
innerLoop: 25600, outerLoop:15625, Time(ms):1120
innerLoop: 5120, outerLoop:78125, Time(ms):1117
innerLoop: 1024, outerLoop:390625, Time(ms):1118
innerLoop: 204, outerLoop:1960784, Time(ms):1146
innerLoop: 40, outerLoop:10000000, Time(ms):1153
innerLoop: 8, outerLoop:50000000, Time(ms):1291
innerLoop: 1, outerLoop:400000000, Time(ms):2539

Reference run (no progress meter):
innerLoop: 16000000, outerLoop:25, Time(ms):1091
innerLoop: 3200000, outerLoop:125, Time(ms):1097
innerLoop: 640000, outerLoop:625, Time(ms):1095
innerLoop: 128000, outerLoop:3125, Time(ms):1090
innerLoop: 25600, outerLoop:15625, Time(ms):1092
innerLoop: 5120, outerLoop:78125, Time(ms):1091
innerLoop: 1024, outerLoop:390625, Time(ms):1097
innerLoop: 204, outerLoop:1960784, Time(ms):1121
innerLoop: 40, outerLoop:10000000, Time(ms):1234
innerLoop: 8, outerLoop:50000000, Time(ms):1263
innerLoop: 1, outerLoop:400000000, Time(ms):2451
Here are my results from YAP threaded:

Code: Select all

Ordinary method:
innerLoop: 16000000, outerLoop:25, Time(ms):1104
innerLoop: 3200000, outerLoop:125, Time(ms):1101
innerLoop: 640000, outerLoop:625, Time(ms):1101
innerLoop: 128000, outerLoop:3125, Time(ms):1106
innerLoop: 25600, outerLoop:15625, Time(ms):1112
innerLoop: 5120, outerLoop:78125, Time(ms):1110
innerLoop: 1024, outerLoop:390625, Time(ms):1116
innerLoop: 204, outerLoop:1960784, Time(ms):1133
innerLoop: 40, outerLoop:10000000, Time(ms):1258
innerLoop: 8, outerLoop:50000000, Time(ms):1262
innerLoop: 1, outerLoop:400000000, Time(ms):2468

Reference run (no progress meter):
innerLoop: 16000000, outerLoop:25, Time(ms):1099
innerLoop: 3200000, outerLoop:125, Time(ms):1095
innerLoop: 640000, outerLoop:625, Time(ms):1095
innerLoop: 128000, outerLoop:3125, Time(ms):1101
innerLoop: 25600, outerLoop:15625, Time(ms):1092
innerLoop: 5120, outerLoop:78125, Time(ms):1098
innerLoop: 1024, outerLoop:390625, Time(ms):1098
innerLoop: 204, outerLoop:1960784, Time(ms):1119
innerLoop: 40, outerLoop:10000000, Time(ms):1232
innerLoop: 8, outerLoop:50000000, Time(ms):1257
innerLoop: 1, outerLoop:400000000, Time(ms):2412
Source for YAP in a module:
(no special compiler restrictions)

Code: Select all

DeclareModule Progress
  
  Declare Init(maxValue)
  Declare Show(value)
  Declare Close()
  
EndDeclareModule

Module Progress
  
  DisableDebugger
  
  ;- Window Constants
  ;
  Enumeration
    #wProgress
  EndEnumeration
  
  ;- Gadget Constants
  ;
  Enumeration  
    #ProgressBar	
  EndEnumeration
  
  ;useful constants
  #timeBeforeWindowOpening = 150 ;(unit: ms) 50 to 500 seems to be reasonable.
  #maxProgressToOpenWindow = 6   ;(unit: parts) 4 means 1/4 (25%). If the job has progressed more than 
                                 ;              this, no progress bar will show up.
  #minInterval = 10              ; minimum time between each update of the progress bar
    
  Global windowOpen, upperValue, progressTimer, totalTimer, interval
  
  Procedure Show(value)
    
    ;{- A progress bar is shown if something takes more than a defined time to complete and less than 
    ; a defined part of the job is done. If the progress has past that part, no progress will show up.
    
    ; The progress bar is updateted depending on how fast the progress is as well as when the 
    ; procedure is called. The slowest of the two decides the update interval.
    
    ; A deeper explanation of the last sentence:
    ; If a job has been completed 4% when the window is opened (all criteria satisfied), the update interval
    ; will be about 2 * #minInterval ms even if the calls only have 2 ms in delay.
    
    ; How to use:
    ; init: Progress::Init(topValue)
    ; show: Progress::Show(partOfTopValue)
    ; quit: Progress::Close()
    ;
    ; All three should be used. #ProgressShow is used inside the loop that should be monitored.
    ; topValue can preferably be the loops stop value and partOfTopValue the counting up (index) in the loop.
    ; Usually the outer loop is the one to monitor.
    ;
    ; Atm #PB_ProgressBar_Smooth is used to show how smooth/jaggy the bar can be, feel free change its looks.
    
    ; There will be a delay of #timeBeforeWindowOpening before testing if the progress is 1 / #maxProgressToOpenWindow
    ; of the upper value
    ; If the progress is less, the progress bar will be shown.
    ; If the progress is more, no progress bar will be shown.
    ; A loop that takes up to #timeBeforeWindowOpening * #maxProgressToOpenWindow milliseconds will not 
    ; show a progress bar. Thats why the product of theese two is important. That's the max time before the
    ; user probably would get worried that the computer didn't respond well to his/her last command. :)
    ;}
    
    ;       ; switch this error check on if you are uncertain of your code - switch it off to save a few cycles
    ; 			If value < 0
    ; 				MessageRequester("ProgressInfo", "value must be set at Show of progress")
    ; 				End
    ; 			EndIf
    If windowOpen = #True
      If ElapsedMilliseconds() - progressTimer > interval			
        SetGadgetState(#ProgressBar, value)
        While WindowEvent()
        Wend
        progressTimer = ElapsedMilliseconds()
      EndIf
    Else
      If ElapsedMilliseconds() > progressTimer And value * #maxProgressToOpenWindow < upperValue
        If OpenWindow(#wProgress, 496, 300, 290, 51, "Busy",  #PB_Window_TitleBar | #PB_Window_BorderLess | #PB_Window_ScreenCentered ) ; "Arbetar"
          StickyWindow(#wProgress, #True)
          ProgressBarGadget(#ProgressBar, 10, 10, 270, 30, 0, upperValue, #PB_ProgressBar_Smooth)
          windowOpen = #True
          
          ; räkna ut ett lagom intervall för uppdateringen
          If value = 0
            interval = #timeBeforeWindowOpening * 80 / 100
          Else
            interval = upperValue / value * 2
            If interval < #minInterval
              interval = #minInterval
            EndIf
            If interval > #timeBeforeWindowOpening * 80 / 100
              interval = #timeBeforeWindowOpening * 80 / 100
            EndIf
          EndIf
          
        EndIf	
      EndIf
    EndIf
        
  EndProcedure
  
  Procedure Init(maxValue)
  
    If maxValue <= 0
      MessageRequester("ProgressInfo", "Top value must be sent as argument at init")
      End
    EndIf
    If #minInterval * 1.25 > #timeBeforeWindowOpening
      MessageRequester("ProgressInfo", "#minInterval must be 25% greater than #timeBeforeWindowOpening")
      End
    EndIf
    progressTimer = ElapsedMilliseconds() + #timeBeforeWindowOpening
    totalTimer = progressTimer
    upperValue = maxValue
    
  EndProcedure
  
  Procedure Close()
  
    If windowOpen = #True
      FreeGadget(#ProgressBar)
      CloseWindow(#wProgress)
      upperValue = 0
      progressTimer = 0
      windowOpen = #False
    EndIf
    
  EndProcedure
  
  DisableDebugger
  
EndModule

#progressTestrun = #True
CompilerIf #progressTestrun
  
  UseModule Progress
  
  Macro Desnug(t)
    CompilerIf #PB_Compiler_Debugger 
      Debug t
    CompilerElse
      PrintN(t)
    CompilerEndIf
  EndMacro
  
  #loopMultiplier = 5
  CompilerIf #PB_Compiler_Debugger = #True
    #totalLoopSize = 40000000
  CompilerElse
    #totalLoopSize = 400000000
  CompilerEndIf
  
  CompilerIf #PB_Compiler_Debugger = #False
    OpenConsole("Progressbar")
  CompilerEndIf
  
  Desnug( "Ordinary method:")
  
  outerLoopTop = 5
  
  While #totalLoopSize / outerLoopTop / #loopMultiplier > 0
    outerLoopTop = #totalLoopSize / (#totalLoopSize / outerLoopTop / #loopMultiplier)
    timeTracker = ElapsedMilliseconds()
    Progress::Init(outerLoopTop)
    For i = 1 To outerLoopTop
      For j = 1 To #totalLoopSize / outerLoopTop
      Next
      Progress::Show(i)
    Next
    Progress::Close()
    Desnug("innerLoop: "+Str(#totalLoopSize/outerLoopTop)+", outerLoop:"+Str(outerLoopTop)+", Time(ms):"+Str(ElapsedMilliseconds() - timeTracker))
  Wend 
  
  
  Desnug("")
  Desnug("Method for tight inner loops:")
  
  outerLoopTop = 5
  
  While #totalLoopSize / outerLoopTop / #loopMultiplier > 0
    outerLoopTop = #totalLoopSize / (#totalLoopSize / outerLoopTop / #loopMultiplier)
    timeTracker = ElapsedMilliseconds()
    Progress::Init(outerLoopTop)
    
    ; a trick to skip some calls to Show()
    loopSkip = Log10(outerLoopTop / 10000) / Log10(2) + 0.5
    If loopSkip < 0: loopSkip = 0: EndIf  
    loopSkip = Pow(2, loopSkip) - 1
    
    For i = 1 To outerLoopTop
      For j = 1 To #totalLoopSize / outerLoopTop
      Next
      ; only Show() when progressed enough
      If i & loopSkip = 0
        Progress::Show(i)
      EndIf
    Next
    Progress::Close()
    Desnug("innerLoop: "+Str(#totalLoopSize/outerLoopTop)+", outerLoop:"+Str(outerLoopTop)+", Time(ms):"+Str(ElapsedMilliseconds() - timeTracker))
  Wend
  
  Desnug("")
  Desnug("Reference run (no progress meter):")
  
  outerLoopTop = 5
  
  While #totalLoopSize / outerLoopTop / #loopMultiplier > 0
    outerLoopTop = #totalLoopSize / (#totalLoopSize / outerLoopTop / #loopMultiplier)
    timeTracker = ElapsedMilliseconds()
    For i = 1 To outerLoopTop
      For j = 1 To #totalLoopSize / outerLoopTop
      Next
    Next
    Desnug("innerLoop: "+Str(#totalLoopSize/outerLoopTop)+", outerLoop:"+Str(outerLoopTop)+", Time(ms):"+Str(ElapsedMilliseconds() - timeTracker))
  Wend
  
  CompilerIf #PB_Compiler_Debugger = #False
    Input()
  CompilerEndIf
  
CompilerEndIf
and here is the code for YAP-threaded:
(compile with 'create threadsafe executeable' on - might work without it, but can fail hard)

Code: Select all

DeclareModule Progress
  
  #noProgress = #False
  Declare Supervise(maxValue, *valueToMonitor.Long)
  Declare Cancel()
  
EndDeclareModule

Module Progress
  
  DisableDebugger
  
  ;- Window Constants
  ;
  Enumeration
    #wProgress
  EndEnumeration
  
  ;- Gadget Constants
  ;
  Enumeration  
    #ProgressBar	
  EndEnumeration
  
  ;useful constants
  #timeBeforeWindowOpening = 150 ;(unit: ms) 50 to 500 seems to be reasonable.
  #maxProgressToOpenWindow = 6   ;(unit: parts) 4 means 1/4 (25%). If the job has progressed more than 
                                 ;              this, no progress bar will show up.
  #minInterval = 10              ; minimum time between each update of the progress bar - 10 ms is fair on a multi core machine
    
  Global windowOpen, upperValue, progressTimer, totalTimer.l, interval
  
  Procedure Show(*valueToMonitor.Long)
    
    ;{- A progress bar is shown if something takes more than a defined time to complete and less than 
    ; a defined part of the job is done. If the progress has past that part, no progress will show up.
    
    ; The progress bar is updateted with #minInterval ms delay
    
    ; How to use:
    ; show if needed: Progress::Supervise(topValue, *valueToMonitor)
    ; force quit: Progress::Close() - can be delayed up to #minInterval ms, but Init should force a wait if 
    ; another loop tries to start a new process.
    ;
    ; Since Supervise() starts a thread, it runs when it needs, how it needs and quits when it needs to.
    ; If a loop has to be ended premature (whith break or something) - close() can be used to quit the progress nicely.
    ;
    ;*******************************************************************************************************
    ; MOST IMPORTANT:
    ; This implementation shall not work. If the debugger is activated, it will kick my code in the ...
    ; ... so use it at your own risk. If strange bugs happens elsewhere in your code - deactivate
    ; the progress info by setting #noProgress = #True
    ; IMPORTANT 1: 
    ; The variable whos pointer you send to Supervise() WILL BE SET TO ZERO, so call Supervise() before you have
    ; started your loop. It's not advisable to start from values other than 0 or 1 since that will affect the 
    ; calculations for opening the progressbar.
    ; IMPORTANT 2:
    ; The progress expects that the valueToMonitor is running uppwards to topValue. 
    ; The process will automatically detect if the valueToMonitor jumps backwards (downwards) and will exit as it 
    ; interprets this as if a new loop has started with the same variable.
    ; If you're lucky to change the value at the right moment and count up to the last reached value fast enough,
    ; you can fool the routine, but don't count on it. ;) The time comes when it catches you cold handed and it will just fold. :)
    ; IMPORTANT 3:
    ; The process will not close until valueToMonitor has reached the defined topValue.
    ; If it's not sure this will happen, use Close() to tell the process you're done with this instance.
    ; IMPORTANT 4:
    ; Supervise() has a feature that makes it wait if there's a window open that havn't closed - as a failsafe it will
    ; shout out to you if it has to wait for to long.
    
    ; There will be a delay of #timeBeforeWindowOpening before testing if the progress is 1 / #maxProgressToOpenWindow
    ; of the upper value
    ; If the progress is less, the progress bar will be shown.
    ; If the progress is more, no progress bar will be shown.
    ; A loop that takes up to #timeBeforeWindowOpening * #maxProgressToOpenWindow milliseconds will not 
    ; show a progress bar. Thats why the product of theese two is important. That's the max time before the
    ; user probably would get worried that the computer didn't respond well to his/her last command. :)
    ;}
    
    While *valueToMonitor\l < upperValue
    
      Select windowOpen
      
        Case #True
          If *valueToMonitor\l < mem.l
            Break ; if loop has restarted - quit fast
          EndIf
          SetGadgetState(#ProgressBar, *valueToMonitor\l)
          While WindowEvent()
          Wend
          mem.l = *valueToMonitor\l
          Delay(#minInterval)
          
        Case -1
        
          Break
          
        Case #False
          If ElapsedMilliseconds() > progressTimer And *valueToMonitor\l * #maxProgressToOpenWindow < upperValue
            If OpenWindow(#wProgress, 496, 300, 290, 51, "Busy",  #PB_Window_TitleBar | #PB_Window_BorderLess | #PB_Window_ScreenCentered ) ; "Arbetar"
              StickyWindow(#wProgress, #True)
              ProgressBarGadget(#ProgressBar, 10, 10, 270, 30, 0, upperValue, #PB_ProgressBar_Smooth)
              windowOpen = #True
              
              ; räkna ut ett lagom intervall för uppdateringen
              If *valueToMonitor\l = 0
                interval = #timeBeforeWindowOpening * 80 / 100
              Else
                interval = upperValue / *valueToMonitor\l * 2
                If interval < #minInterval
                  interval = #minInterval
                EndIf
                If interval > #timeBeforeWindowOpening * 80 / 100
                  interval = #timeBeforeWindowOpening * 80 / 100
                EndIf
              EndIf
              
            EndIf	
          EndIf
          
          Delay(#minInterval)
          
      EndSelect
        
    Wend
    
    If windowOpen = #True Or windowOpen = -1
      FreeGadget(#ProgressBar)
      CloseWindow(#wProgress)
      upperValue = 0
      progressTimer = 0
      windowOpen = #False
    EndIf
    
  EndProcedure
  
  Procedure Supervise(maxValue, *valueToMonitor.Long)

    While windowOpen = #True Or windowOpen = -1 ; se om någon process redan körs som använder fönstret
      Delay(1)
      err + 1
      If err = 500
        MessageRequester("ProgressInfo", "Hey, the process for the busy requester is still running!!!"+#CRLF$+
                                        "Check your previous call (perhaps valueToMonitor didn't reach maxValue")
        End
      EndIf
    Wend
        
    If maxValue <= 0
      MessageRequester("ProgressInfo", "Top value must be sent as argument at init")
      End
    EndIf
    If #minInterval * 1.25 > #timeBeforeWindowOpening
      MessageRequester("ProgressInfo", "#minInterval must be 25% greater than #timeBeforeWindowOpening")
      End
    EndIf
    progressTimer = ElapsedMilliseconds() + #timeBeforeWindowOpening
    totalTimer = progressTimer
    upperValue = maxValue
    
    windowOpen = #False
    
    CompilerIf #noProgress = #False
      *valueToMonitor\l = 0
      CreateThread(@Show(),*valueToMonitor)
    CompilerEndIf
    
  EndProcedure
  
  Procedure Cancel()
  
    windowOpen - 2
    
  EndProcedure
  
  EnableDebugger
  
EndModule

#progressTestrun = #True
CompilerIf #progressTestrun
  
  UseModule Progress
  
  Macro Desnug(t)
    CompilerIf #PB_Compiler_Debugger 
      Debug t
    CompilerElse
      PrintN(t)
    CompilerEndIf
  EndMacro
  
  #loopMultiplier = 5
  CompilerIf #PB_Compiler_Debugger = #True
    #totalLoopSize = 40000000
  CompilerElse
    #totalLoopSize = 400000000
  CompilerEndIf
  
  CompilerIf #PB_Compiler_Debugger = #False
    OpenConsole("Progressbar")
  CompilerEndIf
  
  i.l = 0
  MessageRequester("ProgressInfo", "Demo of progress that is canceled after about 1/4.")
  Progress::Supervise(984250, @i)
  For i = 1 To 984250
    For j = 1 To 4064
    Next
    If i = 250000
      Progress::Cancel()
      Break
    EndIf
  Next
  
  Desnug( "Ordinary method:")
  
  outerLoopTop = 5
  
  While #totalLoopSize / outerLoopTop / #loopMultiplier > 0
    outerLoopTop = #totalLoopSize / (#totalLoopSize / outerLoopTop / #loopMultiplier)
    timeTracker = ElapsedMilliseconds()
    Progress::Supervise(outerLoopTop, @i)
    For i = 1 To outerLoopTop
      For j = 1 To #totalLoopSize / outerLoopTop
      Next
    Next
    Desnug("innerLoop: "+Str(#totalLoopSize/outerLoopTop)+", outerLoop:"+Str(outerLoopTop)+", Time(ms):"+Str(ElapsedMilliseconds() - timeTracker))
  Wend 
  
  Desnug("")
  Desnug("Reference run (no progress meter):")
  
  outerLoopTop = 5
  
  While #totalLoopSize / outerLoopTop / #loopMultiplier > 0
    outerLoopTop = #totalLoopSize / (#totalLoopSize / outerLoopTop / #loopMultiplier)
    timeTracker = ElapsedMilliseconds()
    For i = 1 To outerLoopTop
      For j = 1 To #totalLoopSize / outerLoopTop
      Next
    Next
    Desnug("innerLoop: "+Str(#totalLoopSize/outerLoopTop)+", outerLoop:"+Str(outerLoopTop)+", Time(ms):"+Str(ElapsedMilliseconds() - timeTracker))
  Wend
  
  CompilerIf #PB_Compiler_Debugger = #False
    Input()
  CompilerEndIf
  
CompilerEndIf