The comments to my last practical testing entry got me thinking. The commenter who had located the bug in part 15, which was fixed in part 16, suggested a new approach to the problem and I've been investigating it.
The suggestion is, essentially, to use a timer with a longer range before roll-over rather than
GetTickCount() with its 49.7 day roll-over. In Vista and later we could just use
GetTickCount64() but on earlier platforms that's not available to us. My commenter's solution was to build a
GetTickCount64() on top of
GetTickCount() and use that. Given that adjusting the code for Vista support via the real
GetTickCount64() was on my list of things to do, I decided to also take a look at the potential of the hybrid approach suggested by my commenter.
Switching to using a greater range means that we can remove much of the complexity which was there to protect us from the rollover as this will now only occur after around 584942417.4 years of machine up-time rather than after 49.7 days...
In the zip file that accompanies this article there are two timer queues under test. The first,
CCallbackTimerQueue uses a hybrid
GetTickCount64() implementation that will work on any platform as it uses
GetTickCount() to do the work and the timer queue manages the upper 32-bits itself. The second,
CCallbackTimerQueueEx, uses the real
GetTickCount64() call and will only run on Windows Vista or later platforms. You can build for pre-Vista systems by editing the
Admin\TargetWindowsVersion.h header file and adjusting the values for
The native Vista version of the code is the simplest so I'll discuss that first. There are several additional issues that need to be dealt with if we are building our own
GetTickCount64() and these get in the way of the simpler code...
The first thing, of course, is that I had the tests that were written for the previous versions of the code to make it easier for me to make these changes to the internals of the code. I did this before in part 15 when I fiddled around with the internals to make the code more scalable. The presence of the tests makes this kind of change quite fun; I can concentrate on hacking away at the old design and know that if I change some functionality that is covered by my tests then I should find out as soon as I run the tests. Looking at the header for
CCallbackTimerQueueEx the first thing that you'll notice is that I've removed a couple of constructors; there's now no need to allow the user to tune the maximum timeout allowed. Next you'll see that the actual data structures used for the queue have been simplified; we only need one queue now rather than two and the timers are keyed by
ULONGLONG rather than
DWORD). There are less helper functions and we use an instance of
IProvideTickCount64 rather than
IProvideTickCount. Looking at the code itself, I've hardcoded the maximum timeout to one less than
INFINITE which gives us the whole usable range of a
DWORD for timeouts. I don't see any advantage in expanding the length of the timeouts that you can set to be
ULONGLONGs as 49.7 days should be long enough for anyone ;) and, if it isn't, the user can set another timer when that one expires and build a longer timeout using the current implementation. Since all of the multiple queue stuff can go, setting timers is now simpler and we can go back to the functionality from part 15 where calling
SetTimer() does NOT cause timed out timers to be handled automatically (I was never really comfortable with that change anyway!).
InsertTimer() is simpler as we're only ever dealing with a single timer queue and rather than all the complexity that we had before for dealing with a timer that spans a rollover we can now simply disallow timers that do that; I don't feel too bad about doing this as I think it's reasonable to specify that the code doesn't support setting timers that cross a 584942417.4 years rollover point.
GetNextTimeout() is now massively simplified as all it needs to do is look at the timeout value and compare it with now to see if it has expired. And that's it.
CCallbackTimerQueue is more complex, but not massively so. The complexity arises due to how I maintain the high 32-bits of the 64-bit counter. Since the code works in terms of the 32-bit counter value returned by
GetTickCount() and we know that this wraps every 49.7 days I figure we can spot the wrap (now is less than the last time we checked) and use the event to increment the high 32-bit counter. The only potential risk is that we don't spot the wrap, that is, we don't call
GetTickCount() for 49.7 days and the counter wraps and then becomes more than the last time we called
GetTickCount(). To prevent this unlikely situation, the timer queue sets its own internal maintenance timer for the 32-bit counter roll over point. All this timer does is go off reset itself, but, I think, this is enough to cause
GetTickCount() to be called often enough to prevent any problems...
The tests need to change a little due to the way that
SetTimer() no longer implies
HandleTimeouts() and because of the internal maintenance timer that is set upon construction.
The duplication in the code bothers me, so I expect the next instalment will deal with that, and any bugs that people report!Zermatt.