Friday, August 12, 2011

Online Session Code for Big Objects (Plus a Warning)

In Chapter 9 of More iPhone Development, we wrote a set of classes that mimicked the behavior of GameKit's peer-to-peer connectivity, but for regular network connections (GameKit's only works with BlueTooth and local network connections). Basically, we wrote a class that lets you send and receive anything that can be packaged into an instance of NSData. Since it's relatively trivial to implement NSCoding for most classes, this means passing objects between two iOS apps (or an iOS and a Mac app) becomes pretty easy. You don't have to poll for the data, or worry about chunking out the data. You just make a method call and pass an NSData instance to send data, and then implement a delegate method for receiving data back from the other end. Life is good, right?

Hmm...

Maybe not. There's a pretty big limitation in the book's implementation. That implementation, designed for passing tiny packets of data (TicTacToe game moves), kept everything in memory. If you try to send a good size image to the other connection, likely you'd run out of memory fairly quickly.

A while back, I faced exactly that situation. For a kiosk app that MartianCraft was writing for a client, I needed to send large images shot with a DSLR camera from a Mac Cocoa program to an iPad program and also needed to send pictures taken with the iPad's camera back to the Mac Cocoa app. These images, compressed, ranged from about one to about five megs. I grabbed the OnlineSession class from More, figuring I had the network code basically done, and watched my application go down in a blaze of… well… not glory, that's for sure. Not only did the iPad run out of memory, it ran out of memory FAST… much faster than I expected. Even sending the smaller iPad camera images often caused low-memory crashes.

There were two basic problems with the OnlineSession class when you try to use it for sending larger volumes of data. First, as I said, was that it relied only on physical memory. Given that the physical limitations of the original iPad, this was problematic. But there was another, much bigger problem.

The second issue was that during the process of chunking up the data to send, the code kept making unnecessary copies of the data. Put simply, I made a n00b mistake. The mistake didn't impact the TicTacToe application because the game moves would easily fit into the send buffer, but it's a mistake I've made before and definitely should've known better.

So, what, specifically, was this mistake, you ask?

Using NSData's regular convenience constructor dataWithBytes:length: when creating the new NSData instance to store the portion of the image that won't fit into the send buffer. If you read the description of dataWithBytes:length:, it very clearly says that it makes a copy of the data you provide. So, every time a packet was sent, the code would create a new NSData instance to hold the remainder that wouldn't fit in the buffer, and it would copy all the remaining unsent data for every packet. Ouch.

So, as a simple example, if we were sending a 5 meg image, and the send buffer was set to 128k, the code would make a 4.825 meg copy after the first packet was sent, then a 4.75 meg copy after the second packet was sent, a 4.265 meg copy after the third packet, and so on. After every packet, another slightly smaller copy of the data was made. A descending progression that would eat up memory fast.

After a lot of swearing at myself, I made some modifications to the class to do two things.

First, I switched to using NSData's dataWithBytes:NoCopy:length:, which uses the provided data in place without making a copy. This kept the memory footprint a lot smaller. In some instances, because the DSLR images were so large and our app needed to send so many, I still hit memory problems. So, the second thing I did was to add filesystem caching of the outbound queue so that all the encoded objects waiting to be sent didn't have to fit in memory for the application to function properly.

The new version of the class functions exactly as the one from the book, so you should be able to just drop-in replace the OnlineSession from Chapter 9 with this one without making any changes to your application code.

You can download the new version right here.