I've got a doozey of a problem that I've been working on for a few weeks. It involves stuttering UI performance whenever I save my Core Data managed object context. I've gotten as far as I can on my own and am looking for some help.
The Situation
My application uses two NSManagedObjectContext
instances. One belongs to the application delegate and has a persistent store coordinator attached to it. The other is a child of the main MOC and belongs to a Class
object, called the PhotoFetcher
. It uses NSPrivateQueueConcurrencyType
so all operations performed on this MOC take place in a background queue.
Our application downloads JSON data representing data about photographs from our API. In order to retrieve data from our API, the following sequence of steps takes place:
- Construct an
NSURLRequest
object and use theNSURLConnectionDataDelegate
protocol to construct the data returned from the request, or handle errors. - Once the download of the JSON data is complete, perform a block on the secondary MOC's queue that does the following:
- Parse the JSON using
NSJSONSerialization
into Foundation class instances. - Iterate over the parsed data, inserting or updating entities in my background context as needed. Typically, this results in about 300 new or updated entities.
- Save the background context. This propagates my changes to the main MOC.
- Perform a block on the main MOC to save it's context. This is to have our data persisted to disk, a
SQLite
store. Finally, make a callback to a delegate informing them that the response has been fully inserted into the Core Data store.
- Parse the JSON using
The code to save the background MOC looks something like this:
[AppDelegate.managedObjectContext performBlock:^{
[AppDelegate saveContext]; //A standard save: call to the main MOC
}];
When the main object context saves, it's also saving a fair number of JPEGs that have been downloaded since the last time a main object context save occurred. Currently, on the iPhone 4, we're downloading 15 200x200 JPEGs at 70% compression, or about 2MB of data in total.
The Problem
This works, and works well. My problem is that once the background context saves, the NSFetchedResultsController
running in my view controller picks up the changes propagated up to the main MOC. It inserts new cells in our PSTCollectionView
, an open-source clone of UICollectionView
. While it's inserting new cells, the main context saves and writes those changes to disk. This can take, on an iPhone 4 running iOS 5.1, anywhere from 250-350ms.
During that third of a second, the app is completely unresponsive. Animations that were in progress before the save are paused and no new user events are sent to the main run loop until the save completes.
I ran our app in Instruments using the Time Profiler to identify what is blocking our main thread. Unfortunately, the results have been rather opaque. This is the heaviest stack trace I get from Instruments.
It appeared to be saving updates to the persistent store, but I couldn't be sure. So I removed any calls to saveContext
at all so the MOC wouldn't touch the disk, and the blocking call on the main thread is still persisting.
The trace, in text form, looks like this:
Symbol Name
-[NSManagedObjectContext(_NestedContextSupport) _parentObjectsForFetchRequest:inContext:error:]
-[NSManagedObjectContext executeFetchRequest:error:]
-[NSManagedObjectContext(_NestedContextSupport) executeRequest:withContext:error:]
_perform
_dispatch_barrier_sync_f_invoke
_dispatch_client_callout
__82-[NSManagedObjectContext(_NestedContextSupport) executeRequest:withContext:error:]_block_invoke_0
-[NSManagedObjectContext(_NestedContextSupport) _parentObjectsForFetchRequest:inContext:error:]
-[NSManagedObjectContext executeFetchRequest:error:]
-[NSPersistentStoreCoordinator executeRequest:withContext:error:]
-[NSSQLCore executeRequest:withContext:error:]
-[NSSQLCore objectsForFetchRequest:inContext:]
-[NSSQLCore newRowsForFetchPlan:]
-[NSSQLCore _newRowsForFetchPlan:selectedBy:withArgument:]
-[NSSQLiteConnection execute]
What I Have Tried
Before we touched the Core Data code, the first thing we did was optimize our JPEGs. We switched to smaller JPEGs and saw a performance boost. Then, we decreased the number of JPEGs we download at a time (from 90 down to 15). This also lead to a significant performance boost. However, we're still seeing 250-350ms-long blocks on the main thread.
The first thing I tried was just getting rid of the background MOC to eliminate the possibility that it might be causing problems. In fact, it made things worse since our update-or-create code was running on the main thread and causing overall animation performance to degrade.
Changing the persistent store to NSInMemoryStoreType
had no effect.
Can anyone point me to the "secret sauce" that will give me the UI performance that background managed object contexts have promised?