Background Managed Object Context Staggers UI Animation
Asked Answered
D

2

12

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:

  1. Construct an NSURLRequest object and use the NSURLConnectionDataDelegate protocol to construct the data returned from the request, or handle errors.
  2. Once the download of the JSON data is complete, perform a block on the secondary MOC's queue that does the following:
    1. Parse the JSON using NSJSONSerialization into Foundation class instances.
    2. 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.
    3. Save the background context. This propagates my changes to the main MOC.
    4. 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.

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.

Instruments Heaviest Stack Trace

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?

Dennard answered 28/10, 2012 at 15:28 Comment(2)
It looks like the objects you're saving are then being faulted on the main thread. Could you run the Core Data profiler to see what causes the faulting? And do you use inverse relationships?Fellow
I'll checkout out the CD inspector, but I do use inver relationships.Dennard
F
0

I have a few guesses, in the order in which you should check them:

  1. It looks an awful lot like whatever is being slow is happening after the data is saved (i.e., it's in a callback). This leads me to it being either the reload of the PTSCollectionView or the re-fetch of the fetched results controller.

  2. Does this issue happen with UICollectionView on iOS 6.x? If not, that would lead me lean on PTSCollectionView.

  3. If it still happens, then that means it's likely not the collection view, but instead the fetched results controller. It sort of looks like, from the stack frames (opaque though they may be) that the fetched results controller is trying to perform a fetch which happens through a dispatch_barrier. Those are used to ensure a block isn't executed until after the barrier is reached. I'm out on a limb here, but you might want to check to see this is because internally, Core Data is saving elsewhere, and is thus delaying the execution of any other fetch requests. Again, that's kind of a wild and uneducated guess. But I'd try without the fetched results controller updating right away and see if your stutter still happens.

Another thing which stands out to me is you are performing a lot of work on a child MOC but then performing the save on the parent. It seems like the bulk of the save should be performed by the child. But it could also be I haven't worked with this part of Core Data in a while :-)

Furry answered 28/10, 2012 at 17:6 Comment(0)
H
6

I will make a few assumptions, but from your description, I think it's reasonable.

First, I assume your main MOC has been created with:

[[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];

I make this assumption because...

  1. You use it as a parent context, so it must be either NSMainQueueConcurrencyType or NSPrivateQueueConcurrencyType.

  2. Since you use it elsewhere, you would only do so if it were guaranteed to be accessed on the main queue.

Now, I also assume that you have connected the main MOC directly to the persistent store coordinator, and not to another parent MOC.

When the background MOC does a save, its changes are propagated up into the main MOC (though they are not yet saved). Since your FRC is attached to the main MOC, it will see these changes immediately.

When you issue the save on the main MOC, your code will block because that save will not return until the save has completed.

So, what you are seeing is completely expected.

There are many options to solve your problem.

My first suggestion would be to create a private-queue MOC and make it be the parent of the main-queue MOC.

This means that any save of the main-queue MOC will not block. Instead, it will "save" the data to the parent, which will then do the actual database saving on its own private-queue, and as a result, in a separate thread, in the background.

Now, that will solve your problem of the main thread blocking. This mechanism also fits well with your child-background MOC that loads the database.

Note, that there are a few bugs related to nested contexts in iOS 5, but if you are targeting iOS 6, most of them have been fixed.

See Core Data could not fullfil fault for object after obtainPermanantIDs for more information.

EDIT

Your assumptions are correct, though I'm afraid I'm targeting iOS 5 and can only have a parent MOC to the main MOC on iOS 6 (it causes a deadlock with the FRC). – Ash Furrow

If you are seeing deadlock, first get rid of your dispatch_sync and performBlockAndWait calls. Using a blocking operation from within the main thread should never happen for anything other than the most simple synchronization (i.e., synchronized read operation from data structure)... and then only if necessary. Also, sync calls can cause unexpected deadlock, so it should be avoided whenever possible, especially when calling any code you do not directly control.

If you can't do that, there are a few other options as well. The one I like the most is to attach the FRC to a private-queue-MOC, and then, "glue" your access to/from the FRC via the main thread. You can dispatch_async to the main thread to handle the delegate updates to the table view (or whatever). For example...

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
    dispatch_async(dispatch_get_main_queue(), ^{
        [tableView beginUpdates];
    });
}

You can create a proxy for the FRC, which is just a frontend to make sure access is appropriately synchronized. It does not have to be a full proxy... just enough for what you use the FRC for... and to ensure that all operations are appropriately synchronized.

It's actually easier than it sounds.

In effect, you will have the exact same setup as you have now, except the "main-MOC" will be a private-queue MOC instead of a main-queue MOC. Thus, your main thread will not block when database access occurs.

However, you need to take a few precautions to make sure you use the FRC in the proper context.

Another option is to use your main-MOC, and FRC like you do now to process changes to the database, but make all modifications of the database go through a separate MOC, attached directly to the persistent store coordinator. This makes changes happen in a separate thread. You then use MOC save notifications to update your context and/or refetch data from the store.

The updates to iOS and OSX fix a lot of issues with nested contexts for Core Data, but there are some things you have to worry about when supporting the previous version.

Harangue answered 29/10, 2012 at 1:15 Comment(2)
Your assumptions are correct, though I'm afraid I'm targeting iOS 5 and can only have a parent MOC to the main MOC on iOS 6 (it causes a deadlock with the FRC).Dennard
The deadlock is due to a problem with iOS 5's FRC, not due to anything in my code. I'll give the other options a shot.Dennard
F
0

I have a few guesses, in the order in which you should check them:

  1. It looks an awful lot like whatever is being slow is happening after the data is saved (i.e., it's in a callback). This leads me to it being either the reload of the PTSCollectionView or the re-fetch of the fetched results controller.

  2. Does this issue happen with UICollectionView on iOS 6.x? If not, that would lead me lean on PTSCollectionView.

  3. If it still happens, then that means it's likely not the collection view, but instead the fetched results controller. It sort of looks like, from the stack frames (opaque though they may be) that the fetched results controller is trying to perform a fetch which happens through a dispatch_barrier. Those are used to ensure a block isn't executed until after the barrier is reached. I'm out on a limb here, but you might want to check to see this is because internally, Core Data is saving elsewhere, and is thus delaying the execution of any other fetch requests. Again, that's kind of a wild and uneducated guess. But I'd try without the fetched results controller updating right away and see if your stutter still happens.

Another thing which stands out to me is you are performing a lot of work on a child MOC but then performing the save on the parent. It seems like the bulk of the save should be performed by the child. But it could also be I haven't worked with this part of Core Data in a while :-)

Furry answered 28/10, 2012 at 17:6 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.