Mr. Cluey
: Classes : Handle Class for HTIMERThe timer functions built into 4Test are a bit awkward to use. They are handle based, which is somewhat clumsy, but also they push a lot of the work onto the programmer.
Consider this example, using the standard timer functions
HTIMER hMyTimer ; hMyTimer = TimerCreate() ; TimerStart (hMyTimer) ; print ( TimerState (hMyTimer) ) ; TimerStop (hMyTimer) ; print ( TimerValue (hMyTimer) ) ; TimerDestroy ( hMyTimer) ;
Not pretty - in particular, the programmer is held responsible for keeping track of the HTIMER value, and making sure that the timer is created and destroyed.
When I first started using timers, I wrote a small library to hide all of the clumsy implementation details. The interface to the library was simple enough, but that simplicity was paid for by adding restrictions to the functionality.
I rediscovered timers when I started experimenting with Handle Classes. Switching my timer functions to methods inside of a class would remove the restrictions that I had run into, at a minimal cost.
The CTimer class, in its simplest form, is just a wrapper around an HTIMER. All of the standard functions for dealing with timers are present, but with an object oriented interface.
type CTIMER is window ;
winclass CTimer
{
HTIMER hMyHandle ;
Create ()
{
hMyHandle = TimerCreate () ;
}
Destroy ()
{
TimerDestroy( hMyHandle ) ;
}
Pause ()
{
TimerPause( hMyHandle ) ;
}
Resume ()
{
TimerResume( hMyHandle ) ;
}
Start ()
{
TimerStart( hMyHandle ) ;
}
TIMERSTATE State ()
{
return ( TimerState ( hMyHandle ) ) ;
}
Stop ()
{
TimerStop( hMyHandle ) ;
}
string Str ()
{
return ( TimerStr(hMyHandle ) ) ;
}
real Value ()
{
return ( TimerValue( hMyHandle ) ) ;
}
}
Nothing terribly tricky or inventive - just a straight forward translation of the standard functions. You can see just how straight forward it is when you look at this translation of the original example
CTIMER MyTimer = CTimer () ; MyTimer.Create() ; MyTimer.Start () ; print ( MyTimer.State () ) ; MyTimer.Stop () ; print ( MyTimer.Value () ); MyTimer.Destroy () ;
Well, I think it has improved a little bit - the fact that timers are implemented using handles is concealed, which leaves us with one less thing to worry about. On the other hand, we are still responsible for creating and destroying the handle.
Most of the awkwardness associated with timers is caused by the fact that you can create and destroy timers independently of starting and stopping them. But how often do you need to do that? As far as I'm concerned, while the timer is not running, I don't need it to exist at all. We can save a step for ourselves by eliminating the Create() and Destroy() methods, merging that functionality into Start() and Stop () ;
type CTIMER is window ;
winclass CTimer
{
HTIMER hMyHandle ;
Pause ()
{
TimerPause( hMyHandle ) ;
}
Resume ()
{
TimerResume( hMyHandle ) ;
}
Start ()
{
hMyHandle = TimerCreate () ;
TimerStart( hMyHandle ) ;
}
TIMERSTATE State ()
{
return ( TimerState ( hMyHandle ) ) ;
}
Stop ()
{
TimerStop( hMyHandle ) ;
TimerDestroy ( hMyHandle ) ;
}
string Str ()
{
return ( TimerStr(hMyHandle ) ) ;
}
real Value ()
{
return ( TimerValue( hMyHandle ) ) ;
}
}
Well, that's better - we've concealed the creation and destruction of the timers from the programmer. Not only is that less work for him, but also we needn't worry about the repercussions of forgetting to delete a timer.
Our sample, however, demonstrates our folly...
CTIMER MyTimer = CTimer () ; MyTimer.Start () ; print ( MyTimer.State () ) ; MyTimer.Stop () ; print ( MyTimer.Value () );
Nice and neat - but what happens when we step into MyTimer.Value()? TimerValue is called with the handle of a destroyed timer, and we get and E_HANDLE exception. Oops.
The information that we want certainly isn't going to change after we have stopped the timer, regardless of whether the timer has been destroyed or not. Thus, the key step is to copy the data that we want locally before destroying the timer. A few member variables will do the trick.
Of course, if the timer is live, we want to get the latest and greatest information, not information left over from when the timer was last stopped. So we try to get it from the timer first, if not, get it from the local variables...
winclass CTimer
{
HTIMER hMyHandle ;
//members for holding information after the timer is destroyed
string _str ;
real _value ;
Pause ()
{
TimerPause( hMyHandle ) ;
}
Resume ()
{
TimerResume( hMyHandle ) ;
}
Start ()
{
hMyHandle = TimerCreate () ;
TimerStart( hMyHandle ) ;
}
TIMERSTATE State ()
{
TIMERSTATE tsTemp ;
do
{
tsTemp = TimerState ( hMyHandle ) ;
}
except
{
tsTemp = TS_STOPPED ;
}
return ( tsTemp ) ;
}
Stop ()
{
TimerStop( hMyHandle ) ;
_str = Str () ;
_value = Value () ;
TimerDestroy ( hMyHandle ) ;
}
string Str ()
{
string sTemp ;
do
{
sTemp = TimerStr ( hMyHandle ) ;
}
except
{
sTemp = _str ;
}
return ( sTemp ) ;
}
real Value ()
{
real rTemp ;
do
{
rTemp = TimerValue ( hMyHandle ) ;
}
except
{
rTemp = _value ;
}
return ( rTemp ) ;
}
}
Hey Presto - it works. Now, those do... except... blocks are not free. They slow down the execution time of the Value function itself, which may impact your testing. I'm inclined to think that it won't hurt much. The difference between the original version and this one is 17.5 seconds over 1,000,000 calls. Not insignificant - but the timers are only accurate to milliseconds anyway.
I must admit that I don't like maintaining those do... except... blocks in parallel, but I don't see an alternative. I tried isolating them in a single routine (passing in strings to be unwound with the reference operator) but that doubled the execution time of the Value() method. Twice something very small is still very small though - it might be worth the price.
Another possibility is to restrict the calls still further - if you eliminate the requirement of getting the value while the timer is running, then the performance of the Value() method becomes a non issue. You can, of course, pack as much baggage as you want into the except block - you never fall into that while the timer is running.
If all else fails, create two methods - one to get the value while the timer is running, another to get the value when the timer has stopped - and force your programmers to use them correctly. This should be your last resort, of course.
It's not unreasonable to suppose that, in the course of timing a process, you might want to record some of the intermediate times as well.
The Lap() method is designed to do exactly that - it records splits in a list while the timer is running. You might use it like so...
MyTimer.Start () ; Step1() ; MyTimer.Lap () ; Step2() ; MyTimer.Lap () ; Step3() ; MyTimer.Stop () ;
The Lap method is easy enough to create. We need a member variable to maintain the list of lap times, the method itself to append those times to the list, and a small change to the Start() method to ensure that any previous times are cleared before we begin...
winclass CTimer
{
... //see above
list _lap ;
Start ()
{
hMyHandle = TimerCreate () ;
_lap = {} ;
TimerStart( hMyHandle ) ;
}
Lap ()
{
ListAppend( _lap, Value () ) ;
}
...// see above
}
Assuming that you didn't want your programmers manipulating _lap directly, you might also write some access functions for it.
Now, having implemented the Lap method above, I wanted to get an idea of what kind of impact that it might have on the times recorded by the CTimer. So I ran this code fragment to get some numbers
main ()
{
HTIMER ht = TimerCreate () ;
CTIMER MyTimer = CTimer () ;
integer i ;
MyTimer.Start () ;
TimerStart( ht ) ;
for i = 1 to 100000
MyTimer.Lap () ;
TimerStop( ht ) ;
MyTimer.Stop () ;
print ( TimerValue( ht ) ) ;
print ( MyTimer.Value () ) ;
TimerDestroy ( ht ) ;
}
As you would expect, MyTimer and ht recorded the same times - 15.262000. It occurred to me that it might be more accurate to Pause() MyTimer before getting the lap time and then resume it afterwards. The difference was rather impressive; ht ballooned to 24.765000, the experiment itself was taking 50% longer to run. But MyTimer returned 7.778000 - half the time that MyTimer was recording was storing the lap time in the list. In my scripts, accuracy is more important to me than execution time, so I've implemented Lap () as follows.
winclass MyTimer
{
Lap ()
{
Pause () ;
ListAppend( _lap, Value () ) ;
Resume () ;
}
... //as above
}
Note that this new implementation will throw an E_TIMER exception if you call Lap() while the timer is stopped, which seems like a fine idea.
Using the CTimer, testing the performance of a script (and therefore, the application) is relatively easy to accomplish
// Get Performance Data on Foo(a,b,c) MyTimer.Start () ; Foo ( a, b, c ) ; MyTimer.Stop () ; print ( MyTimer.Value() ) ;
But if you are willing to trade a little accuracy for, you can make this even easier to use. The PerformanceCheck() method effective wraps the Start(), function call, and Stop() into a single bundle....
winclass CTimer
{
PerformanceCheck ( string sFunctionName, varargs of anytype lsArguments )
{
integer ArgCount ListCount( lsArguments ) ;
ArgCount = ListCount( lsArguments ) ;
Start () ;
switch (ArgCount)
{
case 0:
@(sFunctionName) () ;
case 1:
@(sFunctionName) ( lsArguments[1] ) ;
case 2:
@(sFunctionName) ( lsArguments[1] , lsArguments[2] ) ;
case 3:
@(sFunctionName) ( lsArguments[1] , lsArguments[2] , lsArguments[3]) ;
case 4:
@(sFunctionName) ( lsArguments[1] , lsArguments[2] , lsArguments[3], lsArguments[4]) ;
case 5:
@(sFunctionName) ( lsArguments[1] , lsArguments[2] , lsArguments[3], lsArguments[4], lsArguments[5] ) ;
default:
{
raise 123, "PerformanceCheck is not designed to handle this many arguments" ;
}
}
Stop () ;
}
}
The performance check above becomes...
// Get Performance Data on Foo(a,b,c) MyTimer.PerformanceCheck( "Foo" a, b, c ) ; print ( MyTimer.Value() ) ;
I've used this (in the form of a library function as opposed to an object method) but on the whole I don't think that I like it very much. Maybe it deserves points for being clever, but that switch statement is clumsy, and the reference operator is something of a performance hit in the middle of the timing block. Quite frankly, it doesn't simplify things enough to be worth the effort.
| Mr. Cluey : Classes : Handle Class for HTIMER |