Be Lazy! (Another Geeky Tale)
A fair number of years writing software has taught me that it pays to be lazy.
While at WWDC a few weeks ago, I showed the current build of MemoryMiner 2.0 to an Apple engineer who happened to have written IKImageBrowser, the gloriously easy-to-use view for displaying scalable thumbnail images. I would have lovvvvvvvvvvved to have used this class, but like many Apple goodies, it simply didn’t exist four years ago when I first began work on MemoryMiner.
He complimented me on the scaling and scrolling speed of the contact sheet view I had painstakingly created. He also pointed out that the initial load time for a given library has a few seconds of lag as it caches thumbnails. To be precise, MemoryMiner caches the NSCachedBitmapImageRep objects that are drawn when the contact sheet’s thumbnails are being scaled or the user is scrolling. There’s a fair amount of overhead in decompressing a JPEG image before it cn be drawn on screen, which is why I bother to cache this data on disk.
The advantage of this approach is that there’s no need for “placeholder” images for when the thumbnails are first being rendered on screen. IKImageBrowser uses such placeholders, which is one of the reasons why I haven’t switched over to using it. At any rate, last week, I decided to bite the bullet and see what I could do to speed up the caching process. As many people will tell you, you can’t optimize what you haven’t measured, so the first thing I did was create a simple tester application. My tester application let me select a given MemoryMiner library and precisely measure the time it takes to go through the various phases of building up the cache.
A MemoryMiner library stores NSCachedBitmapImageRep data as individual files on disk using the NSArchiver class. In version 1.x of the software, each one of these files is loaded from disk as an NSData object before being un-archived and then subsequently stuffed into an NSDictionary. After launch, the application wasn’t ready for use until the entire cache had been populated. If you’ve got several thousand photos, this could add a few seconds to the launch time: not good. Since there are two separate tasks at hand (reading the NSData objects from disk the and un-archiving the data) I wanted to see which took longer: the test app made short work of this. It turns out that the bottleneck here was the actual un-archiving operation, which didn’t really surprise me.
OS X Leopard introduced the handy dandy NSOperationQueue and NSOperation classes which make it quite easy to break a large task into individual operations (e.g. un archiving some data), throw them on a queue and let the OS figure out how to get the job done using one or more background threads. Lazy. With the release of OS X 10.5.7, you can even uses these classes without your app crashing so I decided to give it a try.
Now, when MemoryMiner is launched I create one NSOperation for each archive file. To do this with even tens of thousands of files takes no time, since I create my NSData objects using the wonderfully lazy NSData method “dataWithContentsOfMappedFile:”. This method doesn’t actually read data from disk until the moment it’s actually needed. Lazy. When you create your NSOperationQueue, you have the opportunity to specify the number of threads that will be created using the “setMaxConcurrentOperationCount:” method. Disk bound operations such as reading data from a file can’t really be parallelized.
The end result: MemoryMiner 2.0 now loads the same library 33% faster than in 1.x. and can load libraries with tens of thousands of images which would otherwise cause a spinning beach ball at launch time in version 1.x. All the caching is done in a background thread so the user can get to the fun stuff right away.
Speaking of fun stuff, it’s time for the Daily Show, so off I go.