Not all time is created equal

While attempting to schedule execution for an exact point in the future, using libdispatch available on iOS and OSX, we came across a quirk when trying to compare two derived times. When most people think of time on a UNIX system, we talk in terms of seconds since the UNIX epoch, i.e. the number of seconds since 1st January, 1970. Indeed, as a UNIX-compliant system, OSX provides the helpful functions time and gettimeofday to do exactly that for us:

NAME
     gettimeofday, settimeofday – get/set date and time

SYNOPSIS
     #include <sys/time.h>

     int
     gettimeofday(struct timeval *restrict tp, void *restrict tzp);

     int
     settimeofday(const struct timeval *tp, const struct timezone *tzp);

DESCRIPTION
     The system's notion of the current Greenwich time and the current time zone is obtained with the gettimeofday() call, and set
     with the settimeofday() call.  The time is expressed in seconds and microseconds since midnight (0 hour), January 1, 1970.

The functions available to us through libdispatch, dispatch_time() and dispatch_walltime(), operate on an opaque type called dispatch_time_t, ultimately defined as a uint64_t. Let's assume that we start with a nicely defined UNIX epoch that we need to convert into a dispatch_time_t.

The following code retrieves the current time in the relevant format, sleeps for a second, and then retrieves the time again. For the sake of brevity, all error-checking has been removed.

void get_initial_timespec(struct timespec* const __nonnull t)
{
    struct timeval now;
    gettimeofday(&now, NULL);
    t->tv_sec  = now.tv_sec;
    t->tv_nsec = now.tv_usec * 1000;
}

int main()
{
    struct timespec initialTimeSpec = { 0 };
    get_initial_timespec(&initialTimeSpec);

    sleep(1);

    struct timespec laterTimeSpec = { 0 };
    get_initial_timespec(&laterTimeSpec);

    printf("Later UNIX time (%ld) %s initial UNIX time (%ld)\n",
            laterTimeSpec.tv_sec,
            laterTimeSpec.tv_sec < initialTimeSpec.tv_sec ? "<" : ">=",
            initialTimeSpec.tv_sec);
}

The short program above gives the following output (at the time of writing):

Later UNIX time (1646764561) >= initial UNIX time (1646764560)

Seems legit. Now, if we convert those times to their dispatch_time_t equivalents:

dispatch_time_t initialDispatchTime = dispatch_walltime(&initialTimeSpec, 0);
dispatch_time_t laterDispatchTime   = dispatch_walltime(&laterTimeSpec, 0);

printf("Later dispatch time (%llu) %s initial dispatch time (%llu)\n",
        laterDispatchTime,
        laterDispatchTime < initialDispatchTime ? "<" : ">=",
        initialDispatchTime);

And run it, we see some very curious output:

Later dispatch time (16799979511846996616) < initial dispatch time (16799979512851579616)

But how can that be? As is almost always the case, the answer is actually right there in the relevant man page for dispatch_time:

NAME
     dispatch_time, dispatch_walltime – Calculate temporal milestones

SYNOPSIS
     #include <dispatch/dispatch.h>

     static const dispatch_time_t DISPATCH_TIME_NOW = 0ull;
     static const dispatch_time_t DISPATCH_WALLTIME_NOW = ~1ull;
     static const dispatch_time_t DISPATCH_TIME_FOREVER = ~0ull;

     dispatch_time_t
     dispatch_time(dispatch_time_t base, int64_t offset);

     dispatch_time_t
     dispatch_walltime(struct timespec *base, int64_t offset);

DESCRIPTION
     The dispatch_time() and dispatch_walltime() functions provide a simple mechanism for expressing temporal milestones for use
     with dispatch functions that need timeouts or operate on a schedule.

     The dispatch_time_t type is a semi-opaque integer, with only the special values DISPATCH_TIME_NOW, DISPATCH_WALLTIME_NOW and
     DISPATCH_TIME_FOREVER being externally defined.  All other values are represented using an internal format that is not safe
     for integer arithmetic or comparison.  The internal format is subject to change.
  
     [...]

The part of particular relevance to us here - values are represented using an internal format that is not safe for integer arithmetic or comparison - is what scuppered us. Performing any arithmetic reasoning on the difference between two dispatch_time_t values is fundamentally not possible. Use them for scheduling and timing, but do not attempt to reason about the difference between them.

Not all time is created equal!

Previous
Previous

CoreTech Summer Internship: Fred