The JAWS Script Cache Life Cycle (CLC) Bug and Resolution
Doug Lee

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:

November 7, 2019
First publication.

Table of Contents

Executive Summary

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.

Background

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 (ints) 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.

Impact

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:

Typically, the above elapsed time is then compared to a static time limit, such as 500 or 5,000 milliseconds. If the elapsed time is shorter than the static value, the cache is considered valid; otherwise, the cache is rebuilt.

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.

A Real-World Example

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:

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.

A Working Solution

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 < 500
to
if tickAge(gcCache.tc) < 500
If the desired current getTickCount() value is already in variable tc, change
if tc - gcCache.tc < 500
to
if tickAge(gcCache.tc, tc) < 500
This 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