13 July 2008

Make Go Fast NOW!

Posted by Louis

Launching Gifter was really hectic for us. We had a lot of last minute issues with our providers, and some of our data pushes were not happening properly when the app launched. Because of that I spent most of the first weekend it was out in emergency mode. Now I am back to work on 1.1. It has a few bug fixes, quite a bit of performance work, a bit more UI polish, and one or two features we dropped from 1.0 at the last minute in order to make the App Store launch day submission deadline. I am really excited about our next release, but more on that another time.

I figured I would actually make a post about writing a good iPhone app. Now the dev tools are still NDA, so I am not going to talk about anything intrinsic to the iPhone yet, but there are a number of topics that are important to the iPhone while not being specific to it.

One of the most important things is for apps to be responsive. They need to scroll fast, they need to get data in front of the user quickly. The more often they stall, the more often the user notices the app getting in their way. That is especially true with the iPhone, where the user is not multitasking, they tend to be using the app in a quick focused way. I think Gifter does well at these things, and we are going to do better in the future.

Let's look at a typical task. In Gifter, when you select a category it queries a server for an XML data file, parses the file, then displays a list of items. That is a pretty common model for iPhone apps. Now consider what happens when you do that. For this example lets assume we are using a 2G iPhone somewhere that gets around 40k/s (some places get better, some get worse). One of our typical data files is around 60K. Now according to my math 60K -> 480k -> 12 seconds. It takes 12 seconds to download a category, plus an additional 2-3 seconds to start up the download between host lookup, network latency, et cetera. That is a lot of time for a user to be staring at a blank screen. And in early betas that is what Gifter did, but we put some work into things, and we think we made it a lot better. Nothing we did was iPhone specific, all of this is equally valid on Mac OS X. In fact the controller and model for Gifter compile and build for Mac OS X (we use the code in some of category management tools). Here is what we did:

1) Trim out all extraneous data
Our data files included a lot of stuff we did not strictly need, or could recalculate on the phone. Part of the reason for that is that Gifter is capable of pulling data directly from vendors in some cases,
and we do not control what they send us. While having that ability is a big win in some cases, we found that by fetching the data on our server we were able to cache, filter, and in some cases compress the data. For instance, we rewrite a lot of URLs to be shorter by using mod_rewrite on our server. (This also allows us to work around some bugs in the iPhone.)

2) Use SAX based parsers
I am a huge fan of using DOM and XPath. It really is much easier to maintain, and it allows me to write code faster. But for DOM to work you need to have the whole document loaded. That means waiting for the download to finish before you start displaying anything. The larger your data is, the worse of a problem this, but it can be pretty bad even for small amounts of data when you are on EDGE. So I grudgingly used SAX* because it is a better experience for the user.

* To be precise, I wrote a tool that builds SAX parsers from XPath triggers. That way I am using SAX parsers, but I do not need to actually write or maintain them.

3) Have your SAX parser incrementally feed your display code
So now you have written your SAX based parser. That does not buy you much  if you hold on to the data until it is done loading before you display it. Make sure to set up your code so that the SAX parser can hand your apps controller one object at a time, which it can insert into the model and view as appropriate.

Okay, so we did all of that, and the app should load faster, and when we test it we see that it does load a little faster. But all of the speed up we have seen is from step 1. It turns out the NSXMLParser requires that it has the whole document loaded before it starts sending events, which basically negates the value of steps 2 and 3. But we can fix that with:

4) Replace NSXMLParser
What I did was roll a mostly call compatible replacement for NSXMLParser, LGXMLParser. The big difference is that you can hand LGXMLParser data as you get it and it will pump the events. I am
opensourcing LGXMLParser today under an MIT style license, you can download it here. Just drop two files into your project, and wherever you use NSXMLParser, then modify your NSURLConnection code to feed it data as it receives. No need to change any of your SAX code. For now this is just a sort of throw over the wall release, but feel free to send comments and feedback. If you use it in your app I would love to know, and if you make improvements to it please consider sending me patches.

Conclusion
It used to take us 14-15 seconds to load the data. Step 1 reduced our average file sizes by ~33%, so that got it down to 10-11. Step 2 made it so that we can start displaying data as soon as we have received enough to display anything, which means that while our total download time is still 10-11 seconds, we have the first few items loaded in 2-3 seconds. Once the user has loaded the first screen of stuff we have effectively hidden the other 8 seconds of load time. So while it still
takes ~10 seconds, it feels like ~3. That is a pretty big improvement from the 15 second pause we started with.

Now I just described a lot of things, which are a lot of work. Even moreso for all of the developers new to the iPhone/Mac OS X development. While some of it is clearly code that is intrinsic to
Gifter, some bits of it is fairly generic. Because I want the apps that I use (not just the ones I write) to responsive, I am open-sourcing LGXMLParser. It is not a complete replacement for
NSXMLParser, I have added functionality to it as I have needed it, so if I did not need it, it is not implemented. I imagine I will add more as I go, but patches are always welcome. As I write these stories I may identify other bits of code to release, and build a library around them, but for now I am just releasing the two files. If you want to use them just drop them into your app!