Update: It turns out CACurrentMediaTime() has a significant bug in that it doesn’t count up when an iPhone is in standby mode. Check this update for a better solution.
If you’ve played WallaBee you’ll know that one of the key areas of the app is the Store, a place where you can buy items with the app currency you collect. The Store has a 3 x 4 grid of items with a timer under each; when the timer runs out, a new item is displayed for purchase. This all sounds straightforward, but when the WallaBee API started getting 3 million hits per day, the Store was one of the first areas to suffer. In this article, I want to run through a few of the problems I experienced when building it the first time, and go into details on the improvements I’ve just added in v1.1.5 of the app which will be available this week.
The Original Plan
In my mind, the Store was going to be fairly simple. It had to be the same for everybody so I’d set up a CRON job that would populate a database table with the upcoming items. Every minute, a check would be performed and items would be added to make sure there was something in every spot until the next time the script ran. Every item had a start and end time that was fixed but had a random amount of seconds (between 10 and 45) so that each item would ‘run out’ at different times. Whenever a player visted the Store, an API call would retrieve the items at each position and do some basic math to work out how long it would be until the position needed to be restocked. Finally, there was an API call to reload just a single position which would be called when an item ran out and a new one was needed. Simple!
The problem with this is that WallaBee became popular and the database bore the brunt when a new item was released and the players descended on the Store. This meant that, when a position needed restocking, there could be thousands of requests in the same second all asking the database to tell them what that item should be. Those kinds of numbers don’t work well (it’s basically a denial of service attack) and so the Store would gradually become emptier and emptier as the network activity indicator struggled and the database slowly spat out what items should be where. Another issue was timing; there was a slight overlap between expiration and start times of items and so clever developers could look and work out when a new item was appearing in the Store upto 15 seconds before the item appeared (leading to an unfair advantage). In addition, if the network connection was slow, some players could be behind the live Store by 10 seconds or so meaning they would be at a disadvantage when something new appeared.
Clearly there was work to be done.
I had three key problems to solve.
- The Store had to be synced for everybody. I wouldn’t accept more than a 500ms gap between players regardless of their connection speed.
- It had to be secure with no chance of seeing what item was coming in advance, even if you sniffed the network traffic with something like Charles.
- It needed to be fast. This meant no database calls and as few API calls as possible, especially during peak times.
In addition, the solution needed to be scalable as I would be adding multiple stores in the next major version of the app. The app would have to be able to display around 80 items at a time rather than 12.
The New Plan
My solution was that the server would generate a physical file every minute that showed all of the updates for the next 10 minutes. When a device requested the Store, it would download this file (no database calls) along with a synchronisation timestamp. This file would be unpacked on the device and each item would be stored locally so that the Store could continue on without making any network calls. At some random point before the 10 minutes was up, the device would request the next 10 minutes worth of data and unpack it silently in the background.
Whilst it sounded good on paper, it would require a lot of work to get everything working correctly. Each piece is inter-connected but I’ll try and explain the general timeline of how it works along with a bit of code.
The first thing that needs to happen is that the server has to generate 10 minutes worth of items. I created a new table that would hold the item objects and wrote a basic CRON job that went through each position and randomly chose an item1 along with a random shelf life between 45 seconds and 4 minutes. This runs every minute and ensures that there is always 10 minutes worth of items coming up. A separate CRON job then runs and compiles these items into a single JSON file which is then encrypted with a military grade cipher (more on that shortly). This file is stored locally and updated every minute. The end result is that you can request the file at any time and always be sure that you have the next 10 minutes worth of items.
One of the key things I wanted to ensure was that the Store was secure as I wouldn’t want anybody to sniff the network traffic and see that a new item was coming out in 7 minutes time (it should always be a surprise). To that end, the file is encrypted using AES-256 before being encoded in Base64 for easy transporting. To do this in PHP is fairly simple:
The thing that most people miss off is the padding as the data to encrypt has to be a multiple of 16 characters and the characters you use for the padding follow a specific rule; they use the character code of the number of characters you need to pad (for example, if you have to use 7 characters of padding, you’d do it with 7 x chr(7). Another key thing was to use ‘cbc’ as the mode as the default ‘ecb’ found on many tutorials doesn’t work well on the iPhone.
Speaking of iOS, decrypting this file is fairly simple using the NSData Category from the CommonCrypto section of aqtoolkit; it’ll be decoded into NSData which can then be turned into an NSString before using the native JSON library to convert it into an NSDictionary.
The other key feature that needed to be implemented was that the iOS device should be synced to the server time so that all players would be shown the items at the correct times; we don’t want players to be behind or ahead even though the API would stop anybody from purchasing an item that isn’t released yet.
I looked into a number of options for time syncing but many of them use the built-in clock on the iPhone. This is no good as a player could simply advance their clock manually to see future items. Instead I opted to have the main Store file return a Unix timestamp of the time of the request. This is simple in PHP:
The issue with this is that the time of the request will vary depending on how slow the uplink connection is on the device, and we need a way of accurately monitoring how long it took to download so we can add an offset.
I’ve been using MKNetworkKit for my iOS networking needs (although I’m planning on switching to AFNetworking in the next release) and it provides a useful block-based method on network operations called onUploadProgressChanged. This provides a double which tells you, as a percentage, how much of your upload is done. When it gets to 1.0, that means you have successfully ‘hit’ the API which should tally up to the $_SERVER[‘REQUEST_TIME’] on the server side2.
Now we know exactly what time we hit the server, we need to start some form of timer so we can see how long it takes until the download completes. To do this, we use CACurrentMediaTime()3. This returns a CFTimeInterval which is roughly the amount of time since your device was rebooted; think of it as a UNIX timestamp that resets every time you start up your phone. I get the current media time upon the successful upload completion, and when the download is complete I get it again and subtract the difference (so I can see, for instance, that the download has taken 572.42ms). We then add this to the timestamp retrieved from the server to get the current server time; we’re now in sync!
Rendering the UI
We have the next 10 minutes worth of items stored locally and we know the exact server time, now we just need to render the UI. To do this, we’ll need a timer that runs continuously in the background to tell us what the current time is; we use this to get our items from our local store and render the progress bars underneath each shelf. The obvious choice is NSTimer, but there are a few gotchas. Firstly, if you create an NSTimer which is targetting ‘self’, you’ll create a retain loop; the view controller will never be released until the timer is invalidated4. Secondly, I’m using a UIScrollView with the store and any touches caused the NSTimer to pause (which may account for the time discrepency in older versions). To fix that, you need to add your timer to the main run loop as so:
We now have a timer that ticks every second but I found it important to use our old friend CACurrentMediaTime() again rather than incrementing a timestamp. The reason for this is that the user may switch tabs, receive a phone call, or leave the app. In those situations, you need to be ready to resume when they come back and, presuming that they’re within the window of how much data you have loaded, you can just kick off without reloading everything. The problem is, if you increment the timer every tick, you’ll start deviating from the start time if you jump back and forth between tabs as the timestamp is always rounded. By storing the CACurrentMediaTime() and the original timestamp when you start your first timer, you can be 100% accurate on your times by working out the number of seconds between now and the original CACurrentMediaTime() and adding them to your timestamp.
With every ‘tick’ we need to update the UI. This is fairly straightforward and is a case of looking at each position, working out the percentage remaining, and applying that to the progress bar. When an item is due to run out, simply fade it out a second before it expires and on the next tick fade it back in. This leads to seamless animations where before there was a fade out, network request, then gradual fade in. The results were so stark in comparison that I had to slow down the change animation as, without the network lag, it was too quick!
One final improvement I made was that the ‘ticks’ only happen when you are viewing the Store. Previously, the Store would continuously update in the background (including network requests) leading to a waste of CPU and battery life and making the rest of the app feel sluggish. Now, as soon as you navigate away, the timer is invalidated and nothing is updated. When you come back, the UI is refreshed and the timer is restarted (accurately as mentioned above).
The only thing we need to remember to do periodically is update our local store. When the Store is first loaded, I choose a random time between 6 and 9 minutes to perform an update. This is to try and spread out the number of requests to the Store file as typically we get 50x more users online when a new item comes out. If they all reloaded the Store at the exact same times we’d have hundreds of API requests in the same second still (admittedly it’s less of an issue as they aren’t hitting the database but it pays to spread the load). If the network connection fails or there is a server problem, the Store will try and reload after 5 seconds until such a time that it gets data. Crucially, the Store will keep going until it runs out of items at which point it stays bare until it gets back on track. To account for any deviations, the time sync is also performed on these updates.
The new Store is far superior to the old one in every way. The server is more stable, the data is more secure, and the UI is much snappier. With the changes to the way the Store UI was rendered, the rest of the app feels faster as well and load on the server during peak times should be substantially reduced. Crucially, an issue that has been present for a while with crashes when switching between apps has finally been resolved now that the NSTimer code has been rewritten.
It’s been quite a journey getting to this point but I’ve enjoyed learning about time syncing, encryption, and keeping time on iOS. As with many things, this is a progressive update and is a reminder that there is always more to learn.
Ok, when I say “randomly chose an item” it’s a bit more complex than that. Items are weighted based on release time, set, pricing, and so on and there are a number of custom overrides (i.e. Christmas is weighted down so you hardly see those items whilst the One Set To Rule Them All is weighted up as it is a huge collection and takes people longer to complete). In future versions, there will be multiple stores that each have their own rules (i.e. there might be a Store which only has items that are over 6 months old or only items from a particular series of sets). ↩︎
It’s not exact but it’s within ~200ms which is accurate enough for this. ↩︎
CACurrentMediaTime() is specifically designed for gaming where you need to have time independent of the system clock and that doesn’t need to be reconciled with an actual world time. For example, the early versions of Tiny Tower had a bug in which you could move your system clock forward in order to get around things like “wait 3 hours for restocking”. They now start a CACurrentMediaTime() when you open the app and this is used to track time.. every second, it is checked and the overall time spent is worked out so it knows when 3 hours has passed, not when the timezone says 3 hours has passed. ↩︎
I hadn’t realised this in the previous version and I had an NSTimer for each position of the Store! This lead to a large number of unexplained crashes as memory ran out. Lesson learned. ↩︎