VERSION 1.0 CLASS BEGIN MultiUse = -1 'True END Attribute VB_Name = "stdPerformance" Attribute VB_GlobalNameSpace = False Attribute VB_Creatable = False Attribute VB_PredeclaredId = True Attribute VB_Exposed = False '@module '@description A class which implements a performance testing and optimisation library. 'Use this class to measure performance and optimise your code. '@example Simple example of measuring performance: '```vb 'Const C_MAX as Long = 1000 'With stdPerformance.CreateMeasure("Some test expression", C_MAX) ' Dim v ' For i = 1 to C_MAX ' v = expressionToTest() ' next 'End With '``` '@example Simple example of optimising performance: '```vb 'With stdPerformance.CreateOptimiser() ' 'some heavy code here 'End With '``` 'Declares for performance counters #If Mac Then #If MAC_OFFICE_VERSION >= 15 Then Private Declare Function GetTickCount Lib "/Applications/Microsoft Excel.app/Contents/Frameworks/MicrosoftOffice.framework/MicrosoftOffice" () As Long #Else Private Declare Function GetTickCount Lib "Applications:Microsoft Office 2011:Office:MicrosoftOffice.framework:MicrosoftOffice" () As Long #End If #Else ' Win32 or Win64 #If VBA7 Then Private Declare PtrSafe Function GetTickCount Lib "kernel32" () As Long #Else Private Declare Function GetTickCount Lib "kernel32" () As Long #End If #End If 'Enum for sentry object type Public Enum EPerfObjType iMeasure = 1 iOptimiser = 2 End Enum 'For Performance Optimisation sentries Private Type TOptimiser EnableEvents As Boolean ScreenUpdating As Boolean Calculation As Long End Type 'For Performance Measurement Private Type TMeasure StartTime As Long KeyName As String Divider As Double End Type 'For Measurement storage Private Type FakeDictItem key As String val As Variant End Type Private Type TSingleton FakeDict() As FakeDictItem 'was Private FakeDict() as FakeDictItem End Type Private Type TThis Singleton As TSingleton ObjectType As EPerfObjType Measure As TMeasure Optimiser As TOptimiser End Type Private This As TThis 'Create '@constructor '@param objType - Type of performance object to create. iMeasure - used for measuring performance, iOptimiser - used for optimising performance '@param params as Variant> - Additional params supplied as array. '@returns stdPerformance - Object termination has special behaviour. See Measure and Optimise methods for further details. '@remark 2023-11-23 This function is not intended to be called directly and thus has been made private. Use the Measure or Optimise methods instead. If absolutely required use `protInit`. Private Function Create(ByVal objType As EPerfObjType, ByVal params As Variant) As stdPerformance Set Create = New stdPerformance Call Create.protInit(objType, params) End Function 'Initialises the class '@protected '@param objType - Type of performance object to initialise. ' '* iMeasure - used for measuring performance '* iOptimiser - used for optimising performance '@param params as Variant> - Additional params supplied as array. Public Sub protInit(ByVal objType As EPerfObjType, ByVal params As Variant) This.ObjectType = objType Select Case objType Case iMeasure This.Measure.KeyName = params(0) This.Measure.Divider = params(1) This.Measure.StartTime = GetTickCount() Case iOptimiser 'TODO: Need to make this more application agnostic Dim oApp As Object: Set oApp = Application 'Store vals This.Optimiser.ScreenUpdating = oApp.ScreenUpdating This.Optimiser.EnableEvents = oApp.EnableEvents 'Set vals oApp.ScreenUpdating = params(0) oApp.EnableEvents = params(1) 'Different options for different applications Select Case oApp.Name Case "Microsoft Excel" This.Optimiser.Calculation = oApp.Calculation oApp.Calculation = params(2) End Select End Select End Sub 'Create a measure object '@constructor '@param sProc - Name of method or block to measure '@param nCount - Number of times the block will be ran (used to calculate average time), default = 1. '@returns stdPerformance - Object which upon termination, adds measurement of block to global cache '@example ```vb 'With stdPerformance.CreateMeasure("Some test expression") ' Dim v ' For i = 1 to 1000 ' v = expressionToTest() ' next 'End With '``` '@example '```vb 'Const C_MAX as Long = 1000 'With stdPerformance.CreateMeasure("Some test expression", C_MAX) ' Dim v ' For i = 1 to C_MAX ' v = expressionToTest() ' next 'End With '``` Public Function CreateMeasure(ByVal sProc As String, Optional ByVal nCount As Double = 1) As stdPerformance Set CreateMeasure = Create(iMeasure, Array(sProc, nCount)) End Function 'Optimise '@constructor '@param ScreenUpdating - ScreenUpdating set value '@param EnableEvents - EnableEvents set value '@param Calculation - Calculation set value '@returns stdPerformance - Object termination has special behaviour. See Measure and Optimise methods for further details. '@remark Calculation is defined as long instead of xlCalculation so the function continues to work without compile error in Word, Powerpoint etc. '@example '```vb 'With stdPerformance.CreateOptimiser() ' 'some heavy code here 'End With '``` Public Function CreateOptimiser(Optional ByVal ScreenUpdating As Boolean = False, Optional ByVal EnableEvents As Boolean = False, Optional ByVal Calculation As Long = -4135) As stdPerformance Set CreateOptimiser = Create(iOptimiser, Array(ScreenUpdating, EnableEvents, Calculation)) End Function 'Obtain a measurement '@static '@param sKey - Name of measurement to get '@returns - Average measurement time '@example Get a measurement '```vb 'Const C_MAX as Long = 1000 'With stdPerformance.CreateMeasure("Some test expression", C_MAX) ' Dim v ' For i = 1 to C_MAX ' v = expressionToTest() ' next 'End With ' 'Debug.Print "Average time for Some test expression: " & stdPerformance.Measurement("Some test expression") & " ms" '``` Public Property Get Measurement(ByVal sKey As String) As Double If Me Is stdPerformance Then Dim v: v = getDictItem(sKey) If TypeName(v) = "Variant()" Then Measurement = getDictItem(sKey)(0) Else Measurement = Empty End If End If End Property 'AddMeasurement '@protected '@param sKey - Name of measurement to add to global cache '@param time - time to add to global cache '@param nCount - number of operations (divisor) '@remark If a time is added that was previously also added then the average of the times is calculated. Public Sub AddMeasurement(ByVal sKey As String, ByVal time As Double, ByVal nCount As Double) If Me Is stdPerformance Then Debug.Print sKey & ": " & time & " ms" & IIf(nCount > 1, " (" & (1000 * time / nCount) & Chr(181) & "s per operation)", "") Dim ind As Long: ind = getDictIndex(sKey) If ind = -1 Then Call setDictItem(sKey, Array(time, 1)) Else Dim vItem: vItem = getDictItem(sKey) Dim average As Long: average = vItem(0) Dim count As Long: count = vItem(1) + 1 average = average + (time - average) / count Call setDictItem(sKey, Array(average, count)) End If End If End Sub 'MeasuresClear - Clears all procedures/blocks and times that have been measured '@static Public Sub MeasuresClear() ReDim This.Singleton.FakeDict(0 To 0) End Sub 'MeasuresKeys '@returns Array - Array containing the procedures or blocks that have been measured. Public Property Get MeasuresKeys() As String() If Me Is stdPerformance Then If UBound(This.Singleton.FakeDict) = 0 Then MeasuresKeys = Split("") Else 'Define return array Dim sOut() As String ReDim Preserve sOut(0 To UBound(This.Singleton.FakeDict) - 1) 'Fill keys array Dim i As Long For i = 0 To UBound(This.Singleton.FakeDict) - 1 sOut(i) = This.Singleton.FakeDict(i).key Next 'return data MeasuresKeys = sOut End If End If End Property 'Used by static class only Private Sub Class_Initialize() If Me Is stdPerformance Then ReDim This.Singleton.FakeDict(0 To 0) End If End Sub 'Used by instance objects only '@destructor Private Sub Class_Terminate() If Not Me Is stdPerformance Then Select Case This.ObjectType Case iMeasure Dim pEndTime As Long: pEndTime = GetTickCount() Call stdPerformance.AddMeasurement(This.Measure.KeyName, Abs(pEndTime - This.Measure.StartTime), This.Measure.Divider) Case iOptimiser 'TODO: Make this more application agnostic Dim oApp As Object: Set oApp = Application 'Store vals oApp.ScreenUpdating = This.Optimiser.ScreenUpdating oApp.EnableEvents = This.Optimiser.EnableEvents 'Different options for different applications Select Case oApp.Name Case "Microsoft Excel" oApp.Calculation = This.Optimiser.Calculation End Select End Select End If End Sub 'FakeDict Helpers '========================================================================================================================================== 'NOTE: These functions are completely unoptimised and are largely in use for the purpose of making this class multi-platform friendly. 'These will be unlikely to be optimised given that this is largely a debugging library. 'Returns the index where a particular key is stored '@param key - Key to find in dictionary '@returns - Index of key in dictionary Private Function getDictIndex(ByVal key As String) As Long On Error GoTo ErrorOccurred Dim i As Long For i = 0 To UBound(This.Singleton.FakeDict) If This.Singleton.FakeDict(i).key = key Then getDictIndex = i Exit Function End If Next On Error GoTo 0 ErrorOccurred: getDictIndex = -1 End Function 'Set an item within a dictionary to a particular value '@param key - Key to find in dictionary '@param v - Value to set dictionary too '@param ind - Index of item. If not given `getDictIndex()` is used Private Sub setDictItem(ByVal key As String, ByVal v As Variant, Optional ByVal ind As Long = -1) 'get index of item in fake dict If ind = -1 Then ind = getDictIndex(key) 'If item not exist, add it If ind = -1 Then ind = getUB(This.Singleton.FakeDict) This.Singleton.FakeDict(ind).key = key ReDim Preserve This.Singleton.FakeDict(0 To ind + 1) End If 'Assign value to index If IsObject(v) Then Set This.Singleton.FakeDict(ind).val = v Else This.Singleton.FakeDict(ind).val = v End If End Sub 'gets the upper bound of an array, if the array is uninitialised return -1 '@param items as Array - Array of dict items. '@returns - Upper bound of array OR -1 if not initialised Private Function getUB(ByRef items() As FakeDictItem) As Long On Error GoTo ErrorOccurred getUB = UBound(items) Exit Function ErrorOccurred: getUB = -1 End Function 'Returns the item paired with some key '@param key - Key to find in dictionary '@returns - Item stored at key Private Function getDictItem(ByVal key As String) As Variant Dim ind As Long: ind = getDictIndex(key) If ind <> -1 Then If IsObject(This.Singleton.FakeDict(ind).val) Then Set getDictItem = This.Singleton.FakeDict(ind).val Else getDictItem = This.Singleton.FakeDict(ind).val End If Else getDictItem = Empty End If End Function