Adding functionality to clsTimer

The entire point of a class is to encapsulate functionality into a container so that as we think of new and exciting ideas we have a place to go to make the changes.  I have been using clsTimer for years.  When writing this blog I got the idea of adding a collection to store a bunch of times.  The other day I was timing a bunch of sql statements which implement steps in a process and it occurred to me that I really wanted to be able to save a note along with the time.  This blog will implement that change to my timer class.  I will also be adding code to log the times into a table, though I will do that in the next blog in order to keep this one a little simpler.  Logging the times allows me to make changes to my sql and process and record how the changes affected the times to accomplish the overall task.

Remember that I discussed using collections of collections in my frameworks prior to learning about classes. I am going to use this as an example of the non-class way of doing thing in order to demonstrate why classes are so invaluable.

I want to save a bunch of timer readings and descriptions of the readings as well.  How can I save a bunch of things in Access?  The obvious solution is to simply log them in a table but unless you go with a disconnected ADO recordset the times to write to the table will affect the timing itself.  Another solution available to us is the generic collection object.  The collection stores variants so we can save any data type into it.  If I want to store a time and its matching description using collections I need to have two collections, one to store the time and another to store the description.  In other words one collection to store every matching piece of data.

Option Compare Database
Option Explicit
‘clsTimerEsoteric

Private Declare Function apiGetTime Lib “winmm.dll” _
Alias “timeGetTime” () As Long
Private mcolTimes As Collection

private mcolDescr as Collection
private mColLabel as Collection

If this is looking messy already, you are right it is messy.  The advantage is that you can key into a collection with a text name, but the disadvantage is needing a collection for every additional element being stored, as well as keeping them all in sync.

Now imagine using two dimensional arrays, with the rows being the times and each column being a property,

Time Descr Label etc

If you use arrays you know they are a mess as well.  Re-dimensioning the array is time consuming so we would need to decide how many rows in advance.  Additionally, with an array we can only use numeric values to index in to the array elements and those end up being “magic numbers” (no obvious description) or you have to dimension and maintain named integer variables to use as the indexes.  Personally I have an aversion to arrays for these reasons.  They are fast but ugly to program and understand.

However if we understand classes, we just build a class to hold each instance of the object (each row in the array) and then use a collection to hold the instances of the class (the rows).  If you work with arrays think of the collection as the rows and the class as all of the columns.  To add a new column to the array we just add a new property to the class.  Plus we can add any code we need to manipulate the data items in the rows.  We can use string values (names)  to key into the collection, and we can add code to add functionality as we shall see momentarily.

Insert a new class and save it as clsTimerCollection.  clsTimerCollection will be what I call a supervisor class, and it will hold instances of clsTimerData.  It will also eventually hold the code to write the time samples to a table at the end of the timing exercise.  clsTimerData will be the time samples as well as a description and label if any.

Now insert another new class and immediately save it as clsTimerData and insert the following code…

‘—————————————————————————————
‘ Module    : clsTimerData
‘ Author    : jwcolby
‘ Date      : 3/3/2013
‘ Purpose   : A class to hold the timer data for a single time sample

‘We are expanding the timer class to allow us to label each sample as well as describe
‘the sample
‘—————————————————————————————
Const cstrModule = “clsTimerData”
Option Compare Database
Option Explicit

Private mlngTime As Long  ‘The ticks at the time the reading was taken
Private mstrDescr As String ‘What the time represents
Private mstrLabel As String ‘A label for the time
Private mblnValid As Boolean

Public Property Get pHasLbl() As Boolean
pHasLbl = Len(mstrLabel) > 0
End Property

Property Get pTime() As Long
pTime = mlngTime
End Property

Property Let pTime(llngTime As Long)
mlngTime = llngTime
mblnValid = True        ‘document that we actually used this instance
End Property

Property Get pDescr() As String
pDescr = mstrDescr
End Property

Property Let pDescr(lstrDescr As String)
mstrDescr = lstrDescr
End Property

Property Get pLabel() As String
pLabel = mstrLabel
End Property

Property Let pLabel(lstrLabel As String)
mstrLabel = lstrLabel
End Property


‘Return just the time as a long
Property Get pTimeLng() As Long
pTimeLng = mlngTime
End Property


‘Return the time as a string formatted as Time: Lbl: Description
‘You can rearrange this code if you prefer a different order.
Property Get pTimeStr() As String
pTimeStr = mlngTime
If Len(mstrLabel) > 0 Then
pTimeStr = pTimeStr & “: ” & mstrLabel
End If
If Len(mstrDescr) > 0 Then
pTimeStr = pTimeStr & “: ” & mstrDescr
End If
End Property

Compile, save and close the class.  Timer data is a simple class that just gives us a couple of variables to store the timer ticks as well as a label and the description, i.e. what the time sample represents..

Notice that we now have three variables in the header to store time sample data as well as a boolean which we set when the time variable is set.  That boolean simply says that we really used this instance.  If we are going to use a label as a key into the collection in the supervisor, then the opportunity exists for the user to ask for a timer instance by a label which does not exist.  Doing so returns an empty clsTimerData instance and this boolean is not set.  I also like to return a formatted string with all of the data and in the spirit of OOP, I have that code in the class ‘closest to’ the data.  Other than that, clsTimerData is pretty simple.

Open clsTimerCollection, and insert the following code…

Option Compare Database
Option Explicit
‘—————————————————————————————
‘ Module    : clsTimerCollection
‘ Author    : jwcolby
‘ Date      : 3/2/2013
‘ Purpose   :

‘This timer class can store multiple times into a collection
‘It can also store a description of the time, why the time was collected.
‘as well as a label.

‘This allows us to collect times during program execution,
‘label the times and add a description of the time
‘store the time in a collection by the label
‘and then look at times later reading them back by the label
‘—————————————————————————————
Const cstrModule = “clsTimerCollection”

‘clsTimerCollection

Private Declare Function apiGetTime Lib “winmm.dll” _
Alias “timeGetTime” () As Long
Private mcolTimes As Collection ‘A collection to store instances of clsTimerData
Private lngStartTime As Long    ‘Store the first time as we initialize the class
Private lngLastReadTime As Long  ‘elapsed ticks when we last called ReadTimer

Private Sub Class_Initialize()
StartTimer
End Sub
Private Sub Class_Terminate()
Set mcolTimes = Nothing
End Sub

Property Get colTimes() As Collection
Set colTimes = mcolTimes
End Property

‘—————————————————————————————
‘ Procedure : ReadTimer
‘ Author    : jwcolby
‘ Date      : 3/3/2013
‘ Purpose   : This is the main reader call.  We can pass in a label and or a description
‘If we pass in a label we will use that as a key in the collection so do not pass in the same
‘label twice
‘—————————————————————————————

Function ReadTimer(Optional lstrLabel As String = “”, Optional strDescr As String = “”) As Long
Dim cTmrData As clsTimerData
Set cTmrData = New clsTimerData ‘Instantiate the class
lngLastReadTime = apiGetTime() – lngStartTime   ‘Get the last read time
cTmrData.pTime = lngLastReadTime    ‘Store it to the time instance
cTmrData.pDescr = strDescr          ‘Along with the description if any
cTmrData.pLabel = lstrLabel         ‘And the label if any
If cTmrData.pHasLbl Then            ‘If there is a label
colTimes.Add cTmrData, cTmrData.pLabel  ‘Use that as the key in the collection
Else
colTimes.Add cTmrData           ‘otherwise just put it in the collection without a key
End If
ReadTimer = cTmrData.pTime
End Function

‘This can be called any time we want to reinitialize the timer class
‘Set the collection to a new object here.
‘This has the effect of clearing the collection if we had been using the
‘timer (and collection) previously.
Sub StartTimer()
Set mcolTimes = New Collection  ‘This is the starting point so reinitialize the collection
lngStartTime = apiGetTime()     ‘and get the starting time
End Sub

‘Simply return the last read time without rereading the timer tick
Function LastReadTime() As Long
LastReadTime = lngLastReadTime
End Function

Property Get pTimesString() As String
Dim cTmrData As clsTimerData
Dim strTmrData As String

For Each cTmrData In mcolTimes
strTmrData = strTmrData & cTmrData.pDescr & vbCrLf
Next cTmrData
pTimesString = strTmrData
End Property
‘—————————————————————————————
‘ Procedure : pTimeByLabel
‘ Author    : jwcolby
‘ Date      : 3/2/2013
‘ Purpose   : Get a specific time back by the label name
‘if there is no clsTimerData instance keyed by the label passed in then
‘return an empty instance.  It is up to the programmer to test for valid data.
‘—————————————————————————————

Property Get pTimeByLabel(lstrLbl As String) As clsTimerData
Dim cTmrData As clsTimerData
On Error Resume Next
Set cTmrData = colTimes(lstrLbl)
If Err <> 0 Then
Set cTmrData = New clsTimerData
End If
Set pTimeByLabel = cTmrData

End Property

‘—————————————————————————————
‘ Procedure : pTimeByIndex
‘ Author    : jwcolby
‘ Date      : 3/16/2013
‘ Purpose   : Get a specific time back by the position in the collection
‘IOW the number of the time data point.
‘if there is no clsTimerData instance keyed by the label passed in then
‘return an empty instance.  It is up to the programmer to test for valid data.
‘—————————————————————————————

Property Get pTimeByIndex(lintIndex As Integer) As clsTimerData
Dim cTmrData As clsTimerData
On Error Resume Next
Set cTmrData = colTimes(lintIndex)
If Err <> 0 Then
Set cTmrData = New clsTimerData
End If
Set pTimeByIndex = cTmrData
End Property

‘—————————————————————————————
‘ Procedure : mTimeToLabel
‘ Author    : jwcolby
‘ Date      : 3/16/2013
‘ Purpose   : Returns the time from the label passed in minus, the start time

‘Since the start time is stored in this class in lngStartTime we can take any
‘clsTimerData instance and get the time between that instance and the start time
‘—————————————————————————————

Function mTimeToLabel(lstrLbl As String) As Long
Dim cTmrForLbl As clsTimerData
On Error GoTo Err_mTimeToLabel

Set cTmrForLbl = colTimes(lstrLbl)
mTimeToLabel = cTmrForLbl.pTimeCurrent – lngStartTime

Exit_mTimeToLabel:
On Error Resume Next
Exit Function
Err_mTimeToLabel:
Select Case Err
Case 0      ‘.insert Errors you wish to ignore here
Resume Next
Case Else   ‘.All other errors will trap
Beep
MsgBox Err.Number & “: ” & Err.Description
Resume Exit_mTimeToLabel
End Select
Resume 0    ‘.FOR TROUBLESHOOTING
End Function
‘—————————————————————————————
‘ Procedure : mTimeToLast
‘ Author    : jwcolby
‘ Date      : 3/16/2013
‘ Purpose   : Gets the time from the start to the last timerdata instance
‘—————————————————————————————

Function mTimeToLast() As Long
Dim cTmrLast As clsTimerData
Set cTmrLast = colTimes(colTimes.Count)
mTimeToLast = cTmrLast.pTimeCurrent – lngStartTime
End Function
‘—————————————————————————————
‘ Procedure : mTimeToIndex
‘ Author    : jwcolby
‘ Date      : 3/16/2013
‘ Purpose   :
‘—————————————————————————————

Function mTimeToIndex(intIndex As Integer)
Dim cTmrIndex As clsTimerData
On Error GoTo Err_mTimeToIndex

Set cTmrIndex = colTimes(intIndex)
mTimeToIndex = cTmrIndex.pTimeCurrent – lngStartTime

Exit_mTimeToIndex:
On Error Resume Next
Exit Function
Err_mTimeToIndex:
Select Case Err
Case 0      ‘.insert Errors you wish to ignore here
Resume Next
Case Else   ‘.All other errors will trap
Beep
MsgBox Err.Number & “: ” & Err.Description & ” – There is no data point # ” & intIndex
Resume Exit_mTimeToIndex
End Select
Resume 0    ‘.FOR TROUBLESHOOTING
End Function

Since we are building this supervisor in the library and since we will want to use it from the application we need to immediately export clsTimerCollection to text file and edit it to make it visible from outside of the library.  Delete the original back in the library and import the edited version back in and save it.  This process was explained in the blog Deeper into Libraries in the section Export and edit class method.  

Having performed the edit and re-imported, compile.  Notice that we get an error in the compile.  The problem is that the method pTimeByLabel tries to return the clsTimerData and we have not yet performed the edit to allow that object to be visible.  The compiler will not allow a visible object to return a non-visible object.  The solution is to export clsTimerData, edit that to make it visible, save, delete clsTimerData from the library and import clsTimerData, save and compile.  The compile should now occur without complaint.

And finally, the test code.  In basTimerTest delete everything out and insert the following:

Option Compare Database
Option Explicit


‘Gives us something to time for testing and demonstrations

Function mclsTimerDemo()
Dim cTmrLoop As clsTimerCollection

Dim Pi As Single

Dim lngOuterCnt As Long
Dim lngInnerCnt As Long
Dim sngVal As Single

Pi = 4 * Atn(1)

Set cTmrLoop = New clsTimerCollection

For lngOuterCnt = 1 To 10

‘This inner loop just does something enough times
‘to cause it to take a measurable time to perform
For lngInnerCnt = 1 To 1000000
sngVal = Pi * lngInnerCnt
Next lngInnerCnt

‘On my virtual development machine this now takes around 13 ms to perform the inner loop
‘Adjust as required for your situation
Debug.Print cTmrLoop.ReadTimer(“InnerLoop” & lngOuterCnt)
Next lngOuterCnt

‘The outer loop takes around 30 seconds
Debug.Print “Outer Loop = ” & cTmrLoop.ReadTimer(“Total Time”)
Debug.Print cTmrLoop.pTimeByLabel(“InnerLoop1”).pTimeStr
Debug.Print cTmrLoop.pTimeByLabel(“InnerLoop2”).pTimeStr
Debug.Print cTmrLoop.pTimeByLabel(“InnerLoop3”).pTimeStr
Debug.Print cTmrLoop.mTimeToLabel(“Total Time”)
Debug.Print cTmrLoop.mTimeToLast()
Debug.Print cTmrLoop.mTimeToIndex(7)
End Function

The biggest difference here is that we can now pass in a label to the timer class so that the timer data is stored keyed on the label.  In the final debug statements we get timer instances back by the label.  We also provided a dedicated method to get the mTimeToLast(), in other words the time from when we started to when we last stored a time data point, as well as a method to retrieve a time by label or index.
All of which is probably overkill until such time as you want to use a timer class which grabs a bunch of times and allows you to look at them later, at which point this kind of stuff becomes useful.  The point really is that doing this kind of thing without classes becomes a royal pain in the neck.  You end up having to use a table to store the time instances.  While that might be useful in itself, it also might slow down the timing.
On the other hand, once you understand classes it becomes almost trivial.  Add a class to hold each data point and a collection to hold the data point instances.  After that the rest is just methods to get at the instances in the collection and stuff like that.

Leave a Reply

Your email address will not be published. Required fields are marked *