The Airsource Blog

Quelle heure est-il? or "What's the time, Mr Jobs?"

A while ago, I was tracking down some NaNs in accelerometer-based code (smoothing device orientation for an OpenGL AR view). It turns out it wasn't my bug — UIAcceleration.timestamp was going backwards approximately every 12 minutes! Naturally, the documentation doesn't mention anything about this:

This value indicates the time relative to the device CPU time base register. Compare acceleration event timestamps to determine the elapsed time between them.

Assuming iPhone OS is similar enough to Mac OS X, it must be using mach_absolute_time():1

Uptime is the highest resolution (64-bits) timer on any PowerPC processor. It counts bus cycles (usually 1/4 the CPU speed) since the last reset (or (re?)boot). All other system timers and clocks are based off of this counter (including gettimeofday).

QA1398 appears to be the earliest2 official documentation of mach_absolute_time() and gives two definitions of GetPIDTimeInNanoseconds(). The first relies on CoreServices' AbsoluteToNanoseconds() which isn't in iPhone OS; we're interested in the second (casts added for a little extra clarity):

// Do the maths.  We hope that the multiplication doesn't
// overflow; the price you pay for working in fixed point.

elapsedNano = (uint64_t)elapsed * (uint32_t)sTimebaseInfo.numer
             / (uint32_t)sTimebaseInfo.denom;

On OS X/i386, it's fine — the several timers are all converted to nanoseconds (this seems to miss the point entirely), so numer = denom = 1 and there's no overflow.

On an iPhone 3G S, it ticks at 24 MHz and (on OS 3) mach_timebase_info() returns 1000000000/24000000.3 The multiply overflows after 264/109 ≈ 1.84e10 ticks — about 768.6 seconds or 12.8 minutes. Bingo.5

But UIAcceleration.timestamp is a double — so it's converting timestamps to nanoseconds, overflowing, and dividing by a billion (or multiplying by 1.0e-9) to get the timestamp in seconds. I'm not sure why they do all this; I'd just calculate numer/(denom*1.0e9) at startup and multiply timestamps by it. It's also pretty easy to convert to nanoseconds without unnecessary6 overflow:

nanos = elapsed/denom*numer + elapsed%denom*numer/denom;

I ended up fixing the bug by throwing away accelerometer updates (but storing the new timestamp) when time has gone backwards. Who cares if you skip an accelerometer update every 12 minutes?

Afterword

I'm pretty sure that mach_absolute_time() is still the "best" way to measure timestamps — it's existed since Mac OS 10.0 and seems to be a highly stable API. CoreAnimation and GCD mention it, along with QA1643 which suggests either using it directly or inlining it. Since iOS 4, there's also CVGetCurrentHostTime() which the docs say is equivalent to the CoreAudio timebase (and thus mach_absolute_time). There's a reasonably accurate article with more details (but I'd divide by 109 instead of multiplying by 10−9).

For completeness, there's also AudioQueueDeviceGetCurrentTime() which requires an audio queue and returns a lot of extra things you probably don't care about. In Snow Leopard/iOS 4 there's [[NSProcessInfo processInfo] systemUptime]. AudioGetCurrentHostTime() exists but is "private" due to the lack of an iPhone OS header (the Mac OS X header says since iPhone OS 2).

I used to be lazy and use CFAbsoluteTimeGetCurrent(), but since iOS 4 it can change arbitrarily (due to app backgrounding, NTP/cell network updates, or leap seconds). [NSDate timeIntervalSinceReferenceDate] is equivalent, [NSDate date] is much slower (~4000 CPU clocks!). Moreover, both seem to round to the microsecond (presumably they're based on gettimeofday()).


  1. UpTime() is the Mac OS 8 name for mach_absolute_time(). On a PowerPC, it retrieves the value of the timebase register using the mftb and mftbu instructions. It's not always the "bus clock"; my iBook G4 says it runs at 18431630 Hz, which doesn't correspond to any bus unless the CPU has a 57.5× multiplier (apparently it'll be a 18.432 MHz UART clock, despite the lack of a serial port). As far as I know, ARMs don't usually have a time base register; mach_timebase_info() on a 3GS takes over 700 CPU clocks, consistent with using an external device. 

  2. Launch Time Performance Guidelines: Measuring Launch Speed: Using Explicit Timestamps has been around for longer and says mach_absolute_time reads the CPU time base register and is the basis for other time measurement functions, but it's unclear which revision it was added in and is more of a brief mention than documentation. Interestingly, it's the only other document mentioning "CPU time base register"

  3. The iPhone and iPhone 3G use 6 MHz. The iPhone 3G S, iPhone 4, and iPad use 24 MHz. The iPod Touches presumably are the same as the corresponding iPhone. On OS 4, mach_timebase_info() reduces the fraction, returning 125/3 on a 3G S, postponing overflow for 195 years, and making it difficult to tell whether UIAcceleration has been fixed.4 

  4. UIAcceleration.timestamp values are still all close to a nanosecond boundary, so it still seems to convert to nanoseconds first. Presumably it'll break noticeably on devices with an odd clock frequency (33.333333 MHz?). It's also odd that it's not fixed given the existence of -[NSProcessInfo systemUptime] which returns a double without nanosecond-rounding. 

  5. It shouldn't be difficult to figure out how many times it's overflowed and thus get the corresponding mach_absolute_time() value, but that just seems like too much effort and is likely to break across releases. 

  6. If the interval is more than 264 ticks, the input has overflowed. If the interval is more than 264 nanoseconds, the output must overflow (or be clipped, or something). Otherwise, it'll work; the "price you pay" for integer arithmetic is just that you need a little more care to prevent intermediate overflow. It's "fixed point" either. 

  7. Interestingly, CMSampleBufferGetSampleTimingInfo() says that frames from AVCaptureVideoDataOutput use a nanosecond timescale, despite the lack of a nanosecond clock.