How can I measure time more precisely in VBA? - excel

I have an Excel document with some VBA scripts.
I'm using 'Timer' to measure execution times of my code.
I am not able to measure how long it takes to add an entry to a Dictionary. The result is 0.
Are there more precise mechanisms available that allow me to measure how long this takes?

At the top of the script you can add this:
Private Declare PtrSafe Function get_frequency Lib "kernel32" _
Alias "QueryPerformanceFrequency" ( _
ByRef Frequency As Currency) _
As Long
Private Declare PtrSafe Function get_time Lib "kernel32" _
Alias "QueryPerformanceCounter" ( _
ByRef Counter As Currency) _
As Long
Then in your code you can get high precision time measurements like this...
' run this once
Dim per_second As Currency: get_frequency per_second
' ...
' get start time
Dim t0 As Currency: get_time t0
' HERE perform the operations you want to time
' ...
' get end time
Dim t1 As Currency: get_time t1
' calculate high precision duration
Dim elapsed_millis As Double: elapsed_millis = (t1 - t0) / per_second * 1000#

Add this at the start of your subroutine:
Dim StartTime As Double 'Declaration of timer to identify length of runtime
StartTime = Timer
At this at the end of your subroutine:
Debug.Print "RunTime for Program is: " & Format((Timer - StartTime) / 86400,
"hh:mm:ss.ms") 'Print out runtime for subroutine

Related

How to create an auto-scroll in Excel VBA that pauses when any key is pressed?

I have a large amount of data to scroll through every day and an autoscroll macro that pauses when a key is pressed (and resumes with a button push) would be a big help.
So far, I've tried:
Sub Autoscroll()
Dim RowCount As Integer
Dim i As Integer
RowCount = Range("Table").Rows.Count
For i = RowCount + 1 To 2 Step -1
Range("A" & i).Select
Application.Wait (Now + TimeValue("0:00:01"))
Next i
End Sub
But this doesn't achieve what I want for a few reasons:
It doesn't pause when I press a key
It can't go faster than 1 second. (I could use the Sleep function to make the scroll move faster)
Looking for some recommendations about the best way to do this.
Thank you
If you insist on using a macro try this, it should do the trick (if you are using Windows!).
You have to press the return key to interrupt. If you'd prefer a different key let me know.
Option Explicit
#If VBA7 Then
Public Declare PtrSafe Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)
Private Declare PtrSafe Function GetAsyncKeyState Lib "user32" (ByVal vKey As Long) As Integer
#Else
Public Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)
Private Declare Function GetAsyncKeyState Lib "user32" (ByVal vKey As Long) As Integer
#End If
'Sub pausing code execution without freezing the app or causing high CPU usage
'Author: Guido Witt-Dörring, https://stackoverflow.com/a/74387976/12287457
Public Sub WaitSeconds(ByVal seconds As Single)
Const VK_RETURN = &HD
Dim currTime As Single, endTime As Single, cacheTime As Single
currTime = Timer(): endTime = currTime + seconds: cacheTime = currTime
Do While currTime < endTime
If GetAsyncKeyState(VK_RETURN) Then
Sleep 200
Do Until GetAsyncKeyState(VK_RETURN)
DoEvents: Sleep 15
Loop
Sleep 200
End If
DoEvents: Sleep 15: currTime = Timer()
'The following is necessary because the Timer() function resets at 00:00
If currTime < cacheTime Then endTime = endTime - 86400! 'seconds per day
cacheTime = currTime
Loop
End Sub
Sub Autoscroll()
Dim RowCount As Long
Dim i As Long
RowCount = Range("Table").Rows.Count
For i = RowCount + 1 To 2 Step -1
WaitSeconds 0.5 '<-- this is how long it waits at every row,
Range("A" & i).Select 'set it to your desired value
Next i
End Sub

Is it possible to use the systemclock to call a sub every 100 milliseconds in Excel VBA?

I am looking for a solution to call a MySub every 100 milliseconds.
MySub contains a loop with a variable length (and variable execution time).
Currently I can do this every second with this code:
Sub UPDATECLOCK()
Call MySub
NextTick = Now + TimeValue("00:00:01")
Application.OnTime NextTick, "UPDATECLOCK"
End sub
I read in the first answer in this post that it is not possible:
VB Excel Macro Millisecond Timing
Although in the second answer this solution is given:
Sub Macro2()
' Macro2 Macro
Range("C1").FormulaR1C1 = "1"
Application.Wait (Now + TimeValue("0:00:01")) '1sec
Range("C1").FormulaR1C1 = "2"
Application.Wait (Now + 0.000001) '1/10sec
Range("C2").Select
Sleep (100)
End Sub
If I understand correctly Application.Wait is added to let Excel wait for several milliseconds.
https://learn.microsoft.com/en-us/office/vba/api/excel.application.wait
Although the Excel application is frozen while using Wait which is not feasible for me.
Maybe an alternative could be to measure the time my loop takes to execute on the system clock.
I found this website https://www.aeternus.sg/best-millisecond-timer-vba/ with this code:
' Performance counter API's
#If VBA7 And Win64 Then
'for 64-bit Excel
Declare PtrSafe Function QueryPerformanceCounter Lib "kernel32" (lpPerformanceCount As Currency) As Long
Declare PtrSafe Function QueryPerformanceFrequency Lib "kernel32" (lpFrequency As Currency) As Long
#Else
'for 32-bit Excel
Declare Function QueryPerformanceCounter Lib "kernel32" (lpPerformanceCount As Currency) As Long
Declare Function QueryPerformanceFrequency Lib "kernel32" (lpFrequency As Currency) As Long
#End If
'Purpose: Measure elapsed time in milliseconds
Sub TimeACode()
Dim curFrequency As Currency
Dim curStartPerformanceCounter As Currency
Dim curEndPerformanceCounter As Currency
Dim lgResult As Long
'obtain no: of counts per second
lgResult = QueryPerformanceFrequency(curFrequency)
If lgResult > 0 Then
Debug.Print "Frequency: " & curFrequency
End If
'measure start count
lgResult = QueryPerformanceCounter(curStartPerformanceCounter)
If lgResult > 0 Then
Debug.Print "Start Count: " & curStartPerformanceCounter
End If
'*****************************************
'Insert the code to measure elapsed time
'*****************************************
'measure end count
lgResult = QueryPerformanceCounter(curEndPerformanceCounter)
If lgResult > 0 Then
Debug.Print "End Count: " & curEndPerformanceCounter
End If
'measure elapsed time
Debug.Print "Elapsed time (ms): " & (curEndPerformanceCounter - curStartPerformanceCounter) / curFrequency
End Sub
I do not understand this code very well. Although it seems to me it counts the processor ticks since the last reboot of the system. Would it be possible to alter this code in a way that it will call MySub after a certain number of ticks of the processor?
If I call this function on my system:
lgResult = QueryPerformanceFrequency(curFrequency)
The function returns 1000.
Does this mean my processor makes 1000 ticks per second?
Would it be possible to call MySub after every 100 ticks?
Thanks a lot!

VBA efficiency test

I found a VBA code to test the run time of a code in the thread How do you test running time of VBA code?
. I implemented and it worked. But every time I run a simple code, as below,it returns me a different result.
I searched and tested many codes, but didn't found what I was expecting.
Is there a way of test the code and return something like the number of clocks that the code demands? Something that every time I run with the code below, returns me the same value?
Sub teste_tempo()
Dim eficiencia As New Ctimer
eficiencia.StartCounter
For i = 0 To 10
i = i + 1
Next i
MsgBox eficiencia.TimeElapsed & "[ms]"
End Sub
Firstly, I did not design this piece of code. I have it in my collection of nice pieces. All the credit must go to the person who created. I found it in many places...
Try this and compare results, please:
Option Explicit
#If VBA7 Then
Private Declare PtrSafe Function getFrequency Lib "kernel32" Alias _
"QueryPerformanceFrequency" (cyFrequency As Currency) As Long
Private Declare PtrSafe Function getTickCount Lib "kernel32" Alias _
"QueryPerformanceCounter" (cyTickCount As Currency) As Long
#Else
Private Declare Function getFrequency Lib "kernel32" Alias _
"QueryPerformanceFrequency" (cyFrequency As Currency) As Long
Private Declare Function getTickCount Lib "kernel32" Alias _
"QueryPerformanceCounter" (cyTickCount As Currency) As Long
#End If
Public Function MicroTimer() As Double
' returns seconds from Windows API calls (high resolution timer)
Dim cyTicks1 As Currency, cyTicks2 As Currency
Static cyFrequency As Currency
MicroTimer = 0
If cyFrequency = 0 Then getFrequency cyFrequency
' get ticks
getTickCount cyTicks1
getTickCount cyTicks2
' calc seconds
If cyFrequency Then MicroTimer = cyTicks2 / cyFrequency
End Function
And use it in the next way:
Sub teste_tempo()
Dim i As Long, dTime As Double
dTime = MicroTimer
For i = 0 To 100000000
i = i + 1
Next i
MsgBox (MicroTimer - dTime) * 1000 & " [ms]"
End Sub
But, it will never return exactly the same passed time!. The Window processed load your CPU and RAM in different percentages for different moments. The differences will be smaller and smaller inverse proportional with the iterations number.

OnTime for less than 1 second without becoming Unresponsive

I have a userform which runs a script every 100ms. The script handles images on the userform and is used to animate them, while the form continues to receive user input (mouse clicks and key presses). This continues until the userform is closed. While Application.OnTime seems to work best, it only operates consistently on time values of 1 second or more.
When I use something like
Sub StartTimer()
Application.OnTime now + (TimeValue("00:00:01") / 10), "Timer"
End Sub
Private Sub Timer()
TheUserForm.ScreenUpdate
Application.OnTime now + (TimeValue("00:00:01") / 10), "Timer"
End Sub
and call StartTimer in the userform, Excel becomes very unresponsive and "Timer" is called many more times per second than it should.
Using the Sleep function causes the program to become unresponsive too, although the script is run with the right interval.
Is there a workaround for this? Thanks in advance!
OnTime can only be scheduled to run in increments of 1 second. When you attempt to schedule it at 1/10th second, you actually schedule at 0 seconds, ie it runs again immediately, consuming all resources.
Short answer, you cannot use OnTime to run an event every 1/10 second.
There are other ways, see CPearson for using a call to Windows API
Public Declare Function SetTimer Lib "user32" ...
Try this simple hybrid method for your 'Timer' sub:
Sub Timer
Application.OnTime now + TimeValue("00:00:01"), "Timer"
t1 = Timer
Do Until Timer >= t1 + 0.9
t2 = Timer
Do Until Timer >= t2 + 0.1
DoEvents
Loop
TheUserForm.ScreenUpdate
... your code
Loop
End Sub
Of course, one problem of user the 'Timer' function is that at midnight your code may turn into a pumpkin (or crash). ;) You would need to make this smarter but if you generally only work during the day, like me, it's not a problem.
Just had this same question today. Here's the solution I was able to find that worked really well. It allows a timed event to fire on intervals as small as 1 millisecond, without taking control of the application or causing it to crash.
The one disadvantage I've been able to find is that TimerEvent() requires a blanket On Error Resume Next to ignore errors caused when it can't execute the code (like when you're editing another cell), which means it will have no idea when a legitimate error occurs.
Declare PtrSafe Function SetTimer Lib "user32" (ByVal hWnd As LongPtr, _
ByVal nIDEvent As LongPtr, ByVal uElapse As Long, ByVal lpTimerFunc As LongPtr) As LongPtr
Declare PtrSafe Function KillTimer Lib "user32" (ByVal hWnd As LongPtr, _
ByVal nIDEvent As LongPtr) As Long
Public TimerID As Long
Sub StartTimer()
' Run TimerEvent every 100/1000s of a second
TimerID = SetTimer(0, 0, 100, AddressOf TimerEvent)
End Sub
Sub StopTimer()
KillTimer 0, TimerID
End Sub
Sub TimerEvent()
On Error Resume Next
Cells(1, 1).Value = Cells(1, 1).Value + 1
End Sub
' yes it is a problem
' it stops when cell input occurs or an cancel = false dblClick
' the timer API generally bombs out EXCEL on these
' or program errors as VBA has no control over them
' this seems to work and is in a format hopefully easy to adapt to
' many simultaneous timed JOBS even an Array of Jobs.. will try it this week
' Harry
Option Explicit
Public RunWhen#, PopIntervalDays#, StopTime#
Public GiveUpDays#, GiveUpWhen#, PopTimesec#, TotalRunSec!
Public PopCount&
Public Const cRunWhat = "DoJob" ' the name of the procedure to run
Sub SetTimerJ1(Optional Timesec! = 1.2, Optional RunForSec! = 10, Optional GiveUpSec! = 20)
If Timesec < 0.04 Then Timesec = 0.05
' does about 150 per sec at .05 "
' does 50 per sec at .6 ????????????
' does 4 per sec at .9 ????????????
'iterations per sec =185-200 * Timesec ( .1 < t < .9 )
' if t >1 as int(t)
' or set Timesec about (iterationsNeeded -185)/200
'
PopTimesec = Timesec
PopIntervalDays = PopTimesec / 86400# ' in days
StopTime = Now + RunForSec / 86400#
GiveUpDays = GiveUpSec / 86400#
TotalRunSec = 0
PopCount = 0
StartTimerDoJob
End Sub
Sub StartTimerDoJob()
RunWhen = Now + PopIntervalDays
GiveUpWhen = Now + GiveUpDays
Application.OnTime RunWhen, cRunWhat, GiveUpWhen
' Cells(2, 2) = Format(" At " & Now, "yyyy/mm/dd hh:mm:ss")
'Application.OnTime EarliestTime:=Now + PopTime, Procedure:=cRunWhat, _
Schedule:=True
End Sub
Sub DoJob()
DoEvents
PopCount = PopCount + 1
'Cells(8, 2) = PopCount
If Now >= StopTime - PopIntervalDays / 2 Then ' quit DoJob
On Error Resume Next
Application.OnTime RunWhen, cRunWhat, , False
Else
StartTimerDoJob ' do again
End If
End Sub
Sub StopTimerJ1()
On Error Resume Next
Application.OnTime EarliestTime:=RunWhen, Procedure:=cRunWhat, _
schedule:=False
End Sub

How to get time elapsed in milliseconds

Since performance of string concatenation is quite weak in VB6 I'm testing several StringBuilder implementations. To see how long they're running, I currently use the built-in
Timer
function which only gives me the number of seconds that have passed after midnight.
Is there a way (I guess by importing a system function) to get something with milliseconds precision?
Yes, you can use the Win32 API:
DWORD WINAPI GetTickCount(void);
To import it in VB6 declare it like this:
Private Declare Function GetTickCount Lib "kernel32" () As Long
Call it before the operation and after and then calculate the difference in time passed.
Put the following code in a Stopwatch class:
Option Explicit
Private Declare Function QueryPerformanceCounter Lib "Kernel32" (X As Currency) As Boolean
Private Declare Function QueryPerformanceFrequency Lib "Kernel32" (X As Currency) As Boolean
Private m_startTime As Currency
Private m_freq As Currency
Private m_overhead As Currency
Public Sub start()
QueryPerformanceCounter m_startTime
End Sub
Public Function ElapsedSeconds() As Double
Dim currentTime As Currency
QueryPerformanceCounter currentTime
ElapsedSeconds = (currentTime - m_startTime - m_overhead) / m_freq
End Function
Public Function ElapsedMilliseconds() As Double
ElapsedMilliseconds = ElapsedSeconds * 1000
End Function
Private Sub Class_Initialize()
QueryPerformanceFrequency m_freq
Dim ctr1 As Currency
Dim ctr2 As Currency
QueryPerformanceCounter ctr1
QueryPerformanceCounter ctr2
m_overhead = ctr2 - ctr1
End Sub
You can use it as follows:
Dim sw as StopWatch
Set sw = New StopWatch
sw.Start
' Code you want to time
Debug.Print "Code took " & sw.ElapsedMilliseconds " ms"
You might also consider using a different approach. Try calling your routines from a loop with enough iterations to give you a measurable time difference.
You can use two Win32 APIs:
QueryPerformanceCounter: To get the count at the start and end of the event.
QueryPerformanceFrequency: To get the number of ticks per second.
These use LARGE_INTEGER to represent 64 bit numbers.
There's code and an explanation in the MSDN KB article Q172338 How To Use QueryPerformanceCounter to Time Code
There's a Thomas Edison story, where he's interviewing some prospective engineers.
He asks them to determine the volume of a light bulb. Candidate A measures it and then uses the formula for the volume of a sphere, and another formula for the volume of the neck, and so on. Candidate B fills it with water and pours it into a measuring cup. Who do you think got the job?
Run it 1000 times and look at your watch before and after. Seconds = milliseconds.
I always use this in a module somewhere (could be in a class though). This code allows you to maintain up to six timers, with high accuracy:
Option Explicit
Private Declare Function QueryPerformanceFrequency Lib "kernel32" (lpFrequency As Currency) As Long
Private Declare Function QueryPerformanceCounter Lib "kernel32" (lpPerformanceCount As Currency) As Long
Private cFrequency As Currency
Private cCounters(0 To 5) As Currency
Public Sub StartCounter(Optional lCounterIndex As Long)
QueryPerformanceFrequency cFrequency
QueryPerformanceCounter cCounters(lCounterIndex)
End Sub
Public Function GetCounter(Optional lCounterIndex As Long) As Double
Dim cCount As Currency
QueryPerformanceFrequency cFrequency
QueryPerformanceCounter cCount
GetCounter = Format$((cCount - cCounters(lCounterIndex) - CCur(0.0008)) / cFrequency, "0.000000000")
End Function
Public Function Scientific(ByVal dValue As Double) As String
Dim lMultiplier As Long
Dim vNames As Variant
lMultiplier = 5
vNames = Array("peta", "tera", "giga", "mega", "kilo", "", "milli", "micro", "nano", "pico", "femto")
If Abs(dValue) < 1 Then
While Abs(dValue) < 1
dValue = dValue * 1000
lMultiplier = lMultiplier + 1
Wend
ElseIf Abs(dValue) >= 1000 Then
While Abs(dValue) >= 1000
dValue = dValue / 1000
lMultiplier = lMultiplier - 1
Wend
End If
Scientific = Format$(dValue, "0.000") & " " & vNames(lMultiplier)
End Function
You can try using the System::Diagnostics::Stopwatch
Imports System.Diagnostics
Dim sw As New Stopwatch()
sw.Start()
// do something
LOG("Elapsed " + sw.ElapsedMilliseconds + " ms")

Resources