This document describes, and presents a solution for, a rare but potentially troublesome condition that can
occur in a JAWS script that uses the GetTickCount
function, especially when using it to manage the life
cycle of a cache.
Document revision history:
A change in JAWS, in or before JAWS 18.0, can cause comparisons of GetTickCount
values to return
unexpected results. The change is the removal, or "masking off," of the highest-order bit in the function's
return value.
The effect is only seen roughly once every 24 days of a computer's running without a reboot.
The effect on cache management code, however, can be that a cache that is intended to be refreshed in a few
seconds or less may survive unchanged for up to 24 days.
The change in JAWS is not itself a bug but was probably meant to make the GetTickCount
value a
more sensible value in most applications.
This document presents a working solution to the problems caused by the change for the cache management scenario.
The JAWS GetTickCount
function returns a value that starts counting at 0 on every reboot of a
Windows computer. The value counts upward in milliseconds. If the computer is left running long enough without a
restart, this value can "wrap around" to 0 and begin counting upward again from there, much like the odometer
in a car.
In older versions of JAWS, the GetTickCount
value was a 32-bit unsigned integer. This meant that
the value would not return to 0 for about 49.7 days. However, the JAWS scripting language treats integers
(int
s) as 32-bit signed values. As a result, the value would suddenly become negative
halfway through this time period.
Due to the way numbers are handled in JAWS scripting and in computers in general, this perhaps odd-looking behavior
had the advantage of allowing "circular" treatment--i.e., subtracting one GetTickCount
value from
another worked out to return the actual time elapsed between the two values, in milliseconds, regardless of
whether the number passed 0 or became negative between the two points of interest.
Such circular treatment is frequently used in the management of data caches, in order to make sure they are
not kept too long without a refresh.
In JAWS 18 or an earlier undetermined version, GetTickCount
became a 31-bit value; that is, the
highest-order bit (bit 32) was removed from the return value. This means that the return value of
GetTickCount()
will never appear as negative in JAWS. However, it also means that the value can
no longer be used in a circular fashion, as is often done when using the function as a means of checking for short
time-elapsed lengths, without a bit of extra care.
Specifically, if the GetTickCount
value passes 0 between two calls and the returned values are
then compared to arrive at an elapsed time, the resulting elapsed-time value will be a negative number far below 0.
A JAWS script that uses GetTickCount
to check the age of a cache will, if exercised at the right
time, fail to register the cache as old even when it is.
This problem may affect many scripts but only under extremely rare conditions. In order for the problem to manifest, all of the following must first occur:
GetTickCount
call must be made shortly before, and another after, the return value of that
function wraps around to 0.
In this scenario, the "elapsed time" will be a negative number far below 0, and will thus appear as less than
the time limit. This condition will persist for over 24 days before GetTickCount
again reaches similar
values to the first one saved above. In practical effect, this means that, under the very rare but possible
conditions outlined here, the cache may become virtually permanent and never update until the next restart of
JAWS or the computer.
The following event occurred on October 10, 2019, and is the event that caused this author to discover the issue presented here. On this day, the author:
GetTickCount
return value wrapped around to 0.
This scenario was caused by a data cache maintained by the SFB scripts not expiring as intended, which was in
turn caused by the GetTickCount
change discussed here.
The following solution effectively recreates the value bit removed by JAWS, so that the functionality of
GetTickCount
in the cache management scenario will be restored.
The code below also returns the absolute value of the difference between tick counts, so that any negative
result will still result in a cache rebuild very quickly.
Usage examples: if gcCache.tc
is the getTickCount
value from when the cache was last
updated, change
if getTickCount() - gcCache.tc < 500to
if tickAge(gcCache.tc) < 500If the desired current
getTickCount()
value is already in variable tc
, change
if tc - gcCache.tc < 500to
if tickAge(gcCache.tc, tc) < 500This approach does create the possibility that a cache update will occur much sooner than usual once every 24.85 days, but the impact of this side effect is of course likely to be negligible.
The below code is hereby placed in the public domain.
int function tickAge(int tc0, optional int tc) ; Return the absolute value of the age, in milliseconds, of the getTickCount() return value passed as tc0. ; If tc is passed, it is used as current time; otherwise, getTickCount() is called for this. ; If tc0 is not an int, a maximal age is returned, on the assumption that this signifies an uninitialized counter. ; This function works around a change in JAWS, before or in 18.0, that masked off the top bit of getTickCount(), ; making circular use impractical without such care as this. ; If used in a JAWS version that does not mask off the top getTickCount() bit, this function will still work. ; This function is meant to be used to determine when to update a data cache for being too old. ; The absolute value is returned to guarantee no possibility of a negative result causing a cache to live ; longer than intended; that is, updating a little too often is better than not often enough. ; This function requires JAWS 14.0 or later due to its use of getVariantType(). if !tc0 && getVariantType(tc0) != VT_Int ; Uninitialized counter; return something like the C maxint. return 0x7FFFFFFFL elif !tc && getVariantType(tc) != VT_Int ; tc was not passed, so use the current value of the counter. tc = getTickCount() endIf if tc < tc0 && tc >= 0 ; getTickCount wrapped past 0 between tc0 and tc without the high-order bit. ; Mere subtraction would give the wrong answer. ; This puts the bit back to make subsequent math work as intended. tc = tc | 0x80000000L endIf var int delta = tc -tc0 ; Absolute value returned just in case of things like an actual tc0-->tc difference ; so large as to cause a huge negative result. ; For the typical use of this function, cache lifecycle maintenance, this should mean the cache is old ; and should be rebuilt. return abs(delta) endFunction