You may remember me extolling the virtues of CACurrentMediaTime() a few days ago in relation to keeping track of time on iOS (specifically in allowing me to sync up the WallaBee Store to the nearest second around the world).
This is a retraction of that.
The problem is that CACurrentMediaTime() has a fatal flaw. Whilst it’s designed to be a counter, it only actually works whilst the iPhone (or iPad) is active. Here is how it’s described by Apple in their documentation:
Returns the current absolute time, in seconds. A CFTimeInterval derived by calling mach_absolute_time() and converting the result to seconds.
Further inspection of mach_absolute_time() gives us very little (documentation is very sparse). On Mac OS X, mach_absolute_time() is based on the last time the device booted (it gives you the number of seconds since you turned your computer on) so it seems prudent that this would be similar on iOS. Apple give some cursory examples of how to convert mach_absolute_time() from nanoseconds into seconds and in doing so point out that “the function is CPU dependent” and needs to be converted by the system rather than by constants. There is no mention of how it all works but the general consensus seems to be that it’s uptime.
Apart from it’s not. Unfortunately for me, it took an App Store release for this bug to be noticed.
See, the interesting thing about mach_absolute_time() is that it appears to be the ‘active’ time of the device, that is to say it’s the number of seconds that you iPhone has been active. However, it’s more sneaky than that as putting your device in standby mode will actually leave the device ‘active’ for a few minutes whilst core components are turned off. In effect, if you are relying on mach_absolute_time() and, by extension, CACurrentMediaTime(), you’ll find that the ticks suddenly pause when your iDevice has been in standby for a few minutes.
So how did this come to light? My game, WallaBee, required a way to count time accurately independent of the system clock (which can be changed by the user). I built a system which allowed me to get the time from my server, account for some lag, and then render elements from CoreData at specific times based on what I believed was a steady ticking in seconds. When the app was sent out to my testers, it seemed to work but occassionally putting the device to sleep and then resuming would lead to some odd behaviour; the clock was running slightly behind. I tried replicating this several times to no avail and eventually left it ‘as is’ since it seemed to be occurring very infrequently and there was no way to work out exactly was happening.
After the code made it into the App Store, there were a few more people commenting that the Store seemed to be running a few seconds behind (or in some cases, minutes behind). I again ran some builds and couldn’t find any errors and it was only after considering that my Xcode build and Testflight build (read “Ad Hoc”) could be different that I discovered the error; any build that wasn’t tethered to my laptop suffered from a slowing down of time. As the Store automatically corrects itself at ~8 minute intervals, and the bug only presented itself after a few minutes of standby, it was quite tricky to replicate exactly.
To test it, I built a very crude mockup app that would present a UIAlertView when you came back from standby mode with the number of seconds perceived by CACurrentMediaTime() and the number of seconds derived from deducting two NSDates (which we couldn’t do in the production app as changing the system clock alters that - it’s good for testing though). Sure enough, after leaving the app in the background whilst the device was on standby for a few minutes, it was apparent that CACurrentMediaTime() was pausing. I did another mockup including mach_absolute_time() and that returned the same result; a pausing of the clock when the device was in standby for a while.
After a lot of googling, I found a solution that was based on the device uptime rather than an arbitrary ‘absolute’ time:
This returns a t_time (read int) of the number of seconds since the device was last powered up and so I was able to slot this into my old code in place of CACurrentMediaTime().
The moral of the story? Always test on an Ad Hoc build that isn’t tethered to Xcode. It’s a pain but the crucial thing in this was that the Xcode debugger was keeping the iPhone alive and that meant I was never able to see that CACurrentMediaTime() was pausing itself. The other thing to do is to read Apple documentation carefully. At no point do they specifically say that mach_absolute_time is based on uptime or that it counts up sequentially, but forum posts and other developers blogs had created that perception based on their own findings and those of the method on OS X. It always pays to be careful and understand fully the methods you are calling.
Fortunately (or not), there was a bug with iOS 5 in the version of WallaBee that was submitted to the App Store and so this fix was bundled in with an expedited review. Of course, that update has now been ‘In Review’ for over 24 hours so we’re not out of the woods yet but at least I’m now using a timer that actually counts up…