How to get time elapsed in milliseconds - string

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")

Related

How can I measure time more precisely in VBA?

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

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!

Why does it take so much time to realease/clear 100'000 objects in Excel-VBA?

Main goal: I'm trying to store and manipulate "OVER 90000!" products in memory from a csv file (one line, one product).
For every product I create an object. That object has multiple String and integer fields:
Option Explicit
Private m_name As String
Private m_price As Integer
Private m_description As String
Private m_amount As Integer
Property Get name() As String
name = m_name
End Property
Property Let name(value As String)
m_name = value
End Property
Property Get price() As Integer
price = m_price
End Property
Property Let price(value As Integer)
m_price = value
End Property
Property Get description() As String
description = m_description
End Property
Property Let description(value As String)
m_description = value
End Property
Property Get amount() As Integer
amount = m_amount
End Property
Property Let amount(value As Integer)
m_amount = value
End Property
Problem: It is relatively fast to creat these object, but it takes a long time to clear/realease them, either with set = nothing or just at the end of the sub. Here's a testcode snippet, which counts the time used for precessing:
Option Explicit
'64 bit Version with "PtrSafe"
Public Declare PtrSafe Function GetTickCount Lib "kernel32.dll" () As Long
Sub TestSub()
Dim t As Long
t = GetTickCount
'Start test code.
TestFunction
'End test code.
Debug.Print GetTickCount - t, , "To delete"
End Sub
Sub TestFunction()
Dim t As Long
t = GetTickCount
'Start test code.
Dim testArray(99999) As TestProduct
Dim i As Long
Dim product As TestProduct
For i = 0 To 99999
Set testArray(i) = New TestProduct
Next
'End test code.
Debug.Print GetTickCount - t, , "To create"
End Sub
Output (milliseconds):
235 To create
7250 To delete
As you can see, it takes almost 8 seconds to free up the memory at the end of the function/sub. I get the same result when I use this code Redim testArray(0) to clear the array.
Is there a better way to store this information in memory? Maybe with arrays or ADODB.recordsets?
I am using a ADODB.Stream to read the UTF-8 coded csv file line by line and store the information in objects.
Thanks for your help!

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.

Resources