NSOperation, NSInvocationOperation and NSURLConnection. Also Showtimes beta

MOST NEW UPDATE: with some tweaking, i've written some safety code that bandages this problem. I don't like writing band-aids, but it was necessary to fix the immediate problem. Showtimes as been tested to run better now, and upgrade to 0.7.2b.is fatally crashing and dead to me for the time being

my pet project Showtimes is an application that finds theaters and movie show times in your area. by providing your zip code, Showtimes uses NSURLConnection to gather a list of theaters in your area (or whatever zip you provided), and a list of movies each theater is showing and what times. All of this information, from one NSURLConnection, however it's limited. All you get form the one request is theater name, movie name, and showtimes, but no synopsis, no movie poster, no actors or else. For all that, we need more NSURLConnections.

in the html file retrieved, for every movie found there is a link to "more information", where the above missing information can be found.

in the first iteration of Showtimes each movie found was added to the "current theater's" movie array. after i finished parsing the html, i then looped through each theater, subsequently looping through each movie array, and creating a new NSURLConnection for each movie in that list and fetching the html, then parsing it for the information.

A problem came up here:

  • the first pass was done asynchronously: you create the connection, give it a delegate and it goes. it's up to you to implement the proper delegate methods to collect the data returned. when it's done, you can parse it with a custom parser, subclass of NSXMLParser. that's where the theaters are created, movies found etc.
  • this doesn't work for loops, because it's asynchronous. creating a URLConnection and giving it a delegate, it will go and fetch, but since it's asynchronous, the loop will go on and re-create everything for the next movie, overwriting the connection etc. you created. so when the first request is wrapping up, the loop as destroyed / recreated the delegate etc and it has no place to go home. it's just lost really. some of it comes back, but it's not reliable.

    the solution to this was to tell NSURLConnection to do this second fetching synchronously, one at a time, so the loop wouldn't continue until the current movie information had been retrieved and parsed. far enough, but performance was horrible, and worse, the synchronous request blocked the main thread, giving the user the appearance of the beach ball of death considering the time it took.

    normally i try not to optimize before things are done, and i didn't feel Showtimes was done (still don't actually), but this was taking too much time to even test. average times were around 30 seconds for 3 theaters with anywhere between 2 - 20 movies each. 30 seconds doesn't sound horrible, but sit and stare at a spinning beach ball for 30 seconds and you'll be convinced the program crashed. not good for the first time you run the application. something had to be done because progress just wasn't feasible when i had to wait 30 some seconds to see if a change worked.

A better way of doing it

first, a design change. i converted the code to instead of adding a movie instance to the theater's list of movies, i added the movie name and it's show times to a dictionary kept by the theater, and added the new movie to a movies array held by the AppController. The reason(s) being that only the show times were unique to a given theater, every other bit of information could be shared across the application. that way, when all the movies are collected, i can then create a new NSURLConnection for each of them ONCE, instead of for each time it's show in a theater.

when all the information was collected, i loop through the array of movies and fetch it's information (poster, synopsis, etc). when that's completed, i cycle through each theater's dictionary from before, and copy the movies that a given theater is showing into it's own movies array, applying it's time (from the dictionary) to that copied instance. ta-da! went from a request-per-movie to request-per-unique movie. no need to get the same data for a movie for each theater it's in.

Insert NSOperation, NSInvocationOperation

that's all great but i'm still blocking the main thread. instead of 30 seconds of beach ball it's down to 15-20. still pretty lame. i had a hunch that the best solution would be a threaded one. i had only one task to complete, but i had to complete it for every movie found.

NSOperation, NSOperationQueue and NSInvocationOperation were added to Mac OS X 10.5 to "simplify the job of executing multiple, finite tasks in a concurrent manner". i took the short cut and just used NSInvocationOperation to call a method on a given object, and added that to the NSOperation Queue, which acts on it's own to preform the task. this was exactly what i wanted, instead of blocking the main thread it's all handled in the background. i threw up a sheet with a status bar to show the user the app is working, not crashing.

the only problem i ran into was knowing when an operation was complete. i didn't find any delegate methods listening to tell me when something finished, so i didn't know when an operation finished (thus a movie had all it's data) and it was ok to go through the array. i worked around this by adding in a part at the end of the specified selector that each invocation was triggering to send an application notification, which AppController listened for. by packing self into the userInfo dictionary, i could determine which movie was done and cycle through the array of theaters, searching with a predicate for that movie that theaters movie dictionary, and making a copy of it's information and applying it's show times. i then incremented a counter and checked it against the count of movies, and close the status sheet when done. overall, it runs pretty smoothly.

March 5th, 2008