Slow deletes and saves in nested NSManagedObjectContext
Asked Answered
R

1

5

In my app I have a "master" NSPrivateQueueConcurrencyType context which serves as a parent to a NSMainQueueConcurrencyType context that the view controller's rely on and pass around. I wanted to do this to take advantage of async saves (this app uses iCloud with Core Data and saves do a lot of work exporting ubiquity logs). The setup is similar to the Zarra approach mentioned at the bottom of this article

Saves within the app typically look like:

[context save:nil];
[context.parentContext performBlock:^{
     [context.parentContext save:nil];
}];

This seems to work well for small edits/updates, but I'm confused about what I see when I delete many objects (eg, delete a project that has hundreds of task objects related to it).

In that situation the save is async but it looks like the main thread gets into a semaphore wait situation (as seen when I pause the debugger) and is effectively blocked (I can't scroll in the UI) until the background private context finishes the save.

Actually, I just noticed that swipe-to-delete deletions of a single object incur a noticeable delay, despite this async save pattern, so it seems all deletions in the app are slow.

If, alternatively, I alloc a new NSPrivateQueueConcurrencyType context, set its persistent store coordinator to the main PSC in the AppDelegate, and perform the delete and save, it still does a lot of work but the UI is never blocked (but then I have to worry about coordinating the contexts, refetching in the main context).

Any ideas what I might have done wrong, or have you seen this behavior also?

Referent answered 3/8, 2012 at 18:40 Comment(0)
T
8

Deletes can take a long time. They have to fix-up all the relationships. There are several things you should do when making large deletions.

First, why does your UI block when the deletions are happening in the background? Because that background thread is really busy doing the deletion block and your UI is trying to fetch to update the data while the delete is happening.

The thread running the actual work is managed by a FIFO queue, so if you give it a large task to execute, it will not be able to do other tasks until the long-running one completes.

You are probably using a FRC, and it is trying to fetch updates when objects and relationships are changing.

If you have a large delete, you may want to change your fetch so it only looks at the current MOC for data while the delete is happening. When the delete is finished, you can then tell the fetch request to go back to querying all the way to the store itself.

Also, when deleting a lot of stuff, it is best to do it in small chunks, inside of an autorelease pool, in a separate thread. From that background thread, delete small chunks at a time, and do it with performBlockAndWait. That way, you do not load up the worker thread's queue with delete requests, and your UI thread can get its requests processed. more quickly.

Alternatively, you can also use advanced features of GCD to have a high-priority and low-priority queue that is used to feed work to the worker thread. You can put your writes on the low priority queue, and your reads on the high priority queue.

Toratorah answered 3/8, 2012 at 20:46 Comment(7)
Thanks Jody! I come across your helpful answers all the time. I'm doing some more work to look into my issues. On one screen I present my Projects (projects have many Tasks) and don't utilize an NSFRC. I'm looking into the things you mentioned for my other screen (which does list the Tasks and uses an NSFRC) where the swipe-to-delete of a single Task is slow using the nested approach (and fast if I revert to the standard NSMainQueueConcurrencyType context tied to the PSC)Referent
Run it in instruments. That's the best way to see exactly what is happening.Toratorah
Strange, using swipe-to-delete results in a save that blocks the UI, but using the Edit-Done button, tapping the red circle, then Delete, doesn't block, despite both flowing through commitEditingStyle:forRowAtIndexPath (where deleteObject: and save: happen)Referent
Capture the tack trace for each of those situations, and you will see what happens before commitEditingStyle:forRowAtIndexPath:.Toratorah
Removed a call that fetched a row (it was called in the swipe-to-delete case, after commitEditingStyle) and the main thread stopped getting blocked. Looking into some issues on another screen where I know I don't fetch anything, and learning more about Instruments; thanks for your help!Referent
Hmm, performing the parent save in performSelector:withObject:afterDelay with a delay of 0.0 prevents the UI lockup (this is on the screen with no NSFRC and delete of an object with a to-many cascading delete relationship)Referent
The parent save happens in a separate thread. The perform...Delay just runs that selector in the next run loop.Toratorah

© 2022 - 2024 — McMap. All rights reserved.