June 96 - Timing On The Macintosh
Timing on the Macintosh
MARTIN MINOW
The Macintosh offers a rich and flexible set of timing operations
that allow you to measure elapsed time, record the time an event
occurs, and schedule actions for future times. This article pulls
together all the available timing options, including the extended
Time Manager and Microseconds routine added with System 7 and
new routines that are available with the PCI-based Macintosh and
Mac OS 8.
You've probably heard the expression, "Time is nature's way of keeping everything
from happening at once." Well, keeping things from happening at the same time is
especially important on computers, and they're particularly good at keeping close
track of time -- both "clock" time and relative time. This article shows you how to
take advantage of the timing options provided on the Macintosh, including new routines
that are available on the PCI-based Macintosh and will also work under Mac OS 8.
There are three common situations in which applications need to keep track of time:
• measuring elapsed time -- for example, for performance analysis or to
see how long it takes the user or some other external entity to perform an
action
• recording an event -- for example, to time-stamp a record in a database
or to inform the user when an action occurs
• scheduling an event -- for example, to start or complete a
time-dependent task
Several timing-related routines are available on the Macintosh, and each is useful in
certain situations. In general, you should:
• Use GetDateTime (or GetTime) if you need to maintain information across
system restarts or need to relate an event to the calendar.
• Use TickCount if you need only a relatively crude measure of time or need
to run under System 6.
• Use the Time Manager's Microseconds routine or Time Manager tasks if
you need improved precision or some attention to drift-free timing. Because
the Time Manager is part of all versions of System 7, it provides the best
service to most clients.
• Use UpTime if you want the highest-precision timing available and run
only on PCI-based Macintosh systems or under Mac OS 8.
This article presents the basics of some standard approaches to the three types of
timing, along with code examples using many of the timing tools at your disposal.
There's also a discussion of factors that can affect the precision of your timing
operations. A simple example of using Microseconds is included on thisissue's CD.
MEASURING ELAPSED TIME
The Macintosh provides several functions that can be used to measure elapsed time.
Your choice of routine depends on the degree of precision you require and the system
software you're running under.
The GetDateTime function returns the current clock time as the number of seconds
since January 1, 1904, and the GetTime function returns the clock time in year,
month, day, hour, minute, and second format (in a date-time record). With their
one-second resolution, however, these functions aren't well suited to measuring
elapsed code performance or the duration of user actions.
January 1, 1904, was chosen as the base for the Macintosh clock
because it was the first leap year of the twentieth century. 1900 wasn't a leap
year because leap years are skipped every 100 years for three centuries. On
the fourth century, which will be the year 2000, the leap year isn't skipped.
This means that by starting with 1904, Macintosh system programmers could
save a half dozen instructions in their leap-year checking code, which they
thought was way cool.*
One of the functions available for finer timing resolution is TickCount, which returns
the time elapsed since the system last started up in units of about 1/60 second. Until
System 7, this was the only reasonable way to measure sub-second intervals. With
System 7, the Microseconds routine became available. (Using the extended Time
Manager is another possible method on System 7, but it's more complicated and so isn't
commonly used for that purpose.) Furthermore, the PCI-based Macintosh (and Mac OS
8) provide UpTime, a replacement for Microseconds.
THE MICROSECONDS ROUTINE
The Microseconds routine returns the number of microseconds that have elapsed since
system startup as an unsigned 64-bit integer and offers a convenient way of timing
events and operations. Theoretically, it can resolve intervals of about 20
microseconds, although in practice it can't time intervals that small (for reasons
given later, in the section on timing accuracy).
The value returned by Microseconds has the UnsignedWide structure, shown in Listing
1. A signed wide structure is used for the result of subtracting two Microseconds
values to calculate elapsed time. UnsignedWide is defined in Types.h of the universal
headers, but is also shown in Listing 1 for convenience.
Listing 1. Microseconds structures
unsigned long hi;
unsigned long lo;
};
typedef struct UnsignedWide UnsignedWide;
signed long hi;
unsigned long lo;
};
typedef struct wide wide;
/*
* The sample code defines a SignedWide structure for consistency.
*/
To time a routine, your application would do the following:
UnsignedWide startTime;
UnsignedWide endTime;
Microseconds(&startTime);
DoMyOperation();
Microseconds(&endTime);
Subtracting startTime from endTime will yield the elapsed time. However, the 64-bit
Microseconds values are rather unwieldy to deal with. The simplest solution is to
convert them to double-precision floating-point numbers. MicrosecondToDouble,
shown in Listing 2, converts a Microseconds value to double-precision floating point.
Using double precision will retain accuracy for all practical purposes. You can also
use integer subtraction to get the difference between the two times and convert the
result to floating point (or whatever you need) afterward. MicrosecondDelta, also in
Listing 2, computes the difference between two Microseconds result values, returning
a signed 64-bit integer to retain precision.
Listing 2. Microseconds routine support functions
#define kTwoPower32 (4294967296.0) /* 2^32 */
double MicrosecondToDouble(register const UnsignedWide *epochPtr)
register double result;
result = (((double) epochPtr->hi) * kTwoPower32) + epochPtr->lo;
return (result);
}
void MicrosecondDelta(register const UnsignedWide *startPtr,
register const UnsignedWide *endPtr,
register SignedWide *resultPtr)
if (endPtr->lo >= startPtr->lo)
resultPtr->hi = endPtr->hi - startPtr->hi;
else
resultPtr->hi = (endPtr->hi - 1) - startPtr->hi;
resultPtr->lo = endPtr->lo - startPtr->lo;
}
If you prefer using only integer arithmetic, the sample code on this
issue's CD includes a very simple -- and very inefficient -- 64-bit integer
library with add, subtract, multiply, and divide functions that can be used to
calculate time values. For a more complete 64-bit integer math library, see
the article "64-Bit Integer Math on 680x0 Machines" in develop Issue 26.*
THE UPTIME ROUTINE
PCI-based Macintosh systems and the Mac OS 8 operating system provide a new
routine, UpTime, that returns the value of the PowerPC internal clock. The value
that's returned has the data type AbsoluteTime and cannot be interpreted directly by
applications, because the units are system dependent and not defined by the API. A
library is provided to convert values of type AbsoluteTime into formats whose units
are known. This approach allows the system to maximize precision and performance.
The time values returned by UpTime start at 0 at system startup and increase
monotonically for as long as it's running. To time a routine with UpTime, your
application might do the following:
AbsoluteTime startTime;
AbsoluteTime endTime;
AbsoluteTime elapsedTime;
Nanoseconds elapsedNanoseconds;
/* This is an UnsignedWide integer */
startTime = UpTime();
DoMyOperation();
endTime = UpTime();
elapsedTime = SubAbsoluteFromAbsolute(endTime, startTime);
elapsedNanoseconds = AbsoluteToNanoseconds(elapsedTime);
These functions and others used to process AbsoluteTime values are described
inDesigning PCI Cards and Drivers for Power Macintosh Computers.
RECORDING EVENT OCCURRENCE
If you need to record when an event occurred (for example, when a record was added to
a database), you can use the value returned by GetDateTime or GetTime. In most
situations, GetDateTime is easier to deal with, being more compact and saving you from
converting days, months, years, and so on into seconds for computations.
Keep in mind that GetDateTime returns the local clock time, which means that you
can't always use its value to determine which of two records is earlier, as they could
have been created in different time zones or under different daylight saving time rules.
If being able to compare times across time zones is important, your application should
call the ReadLocation routine and store its MachineLocation result at the time you
record the event so that the application can compute a location-independent value by
converting the local time to GMT (Greenwich Mean Time).
Unfortunately, the local time value returned by GetDateTime isn't coordinated with the
more precise values returned by Microseconds and UpTime. This makes it difficult to
record local times with fractional second resolution. Listing 3 shows one way to work
around this problem. It's adapted from the LogConvertTimestamp function in my PCI
device driver sample library, which was first published in develop Issue 22
("Creating PCI Device Drivers"). Listing 3 also illustrates my simple 64-bit support
library.
Listing 3. Time of day with fractional second resolution
void LogConvertTimestamp(
AbsoluteTime eventTime, /* Value to convert */
DateTimeRec *eventDateTime, /* Result goes here */
UInt32 *residualNanoseconds /* Fractional second */
)
Nanoseconds eventNanoseconds;
UnsignedWide eventSeconds, temp;
static const UnsignedWide kTenE9 = { 0, 1000000000L };
static UInt32 gUpTimeNumerator;
static UnsignedWide gUpTimeDenominator;
static Nanoseconds gNanosecondsAtStart = { 0, 0 };
/*