Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

CoreData child contexts, NSFetchedResultsController and main thread

Following this excellent post by Olivier Drobnik, I've implemented the three-layer CoreData stack proposed by CoreData guru Marcus S. Zarra:

Three-layer CoreData context architecture

The only difference from this diagram and my code is that I only use one Temporary Background MOC, in order to avoid duplicates when inserting objects in several temp MOCs. Here's my context initialisation code:

#pragma mark - NSManagedObjectContexts

+ (NSManagedObjectContext *)privateManagedObjectContext
{
    if (!_privateManagedObjectContext) {

        // Setup MOC attached to PSC
        _privateManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
        [_privateManagedObjectContext setPersistentStoreCoordinator:[self persistentStoreCoordinator]];

        // Add notification to perform save when the child is updated
        _privateContextSaveObserver =
        [[NSNotificationCenter defaultCenter]
         addObserverForName:NSManagedObjectContextDidSaveNotification
         object:nil
         queue:nil
         usingBlock:^(NSNotification *note) {
             NSManagedObjectContext *savedContext = [note object];
             if (savedContext.parentContext == _privateManagedObjectContext) {
                 [_privateManagedObjectContext performBlock:^{
                     NSLog(@"AMBCoreData -> saving privateMOC");
                     NSError *error;
                     if (![_privateManagedObjectContext save:&error]) {
                         NSLog(@"AMBCoreData -> error saving _privateMOC: %@ %@", [error localizedDescription], [error userInfo]);
                     }
                 }];
             }
         }];
    }
    return _privateManagedObjectContext;
}

+ (NSManagedObjectContext *)mainUIManagedObjectContext
{
    if (!_mainUIManagedObjectContext) {

        // Setup MOC attached to parent privateMOC in main queue
        _mainUIManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
        [_mainUIManagedObjectContext setParentContext:[self privateManagedObjectContext]];

        // Add notification to perform save when the child is updated
        _mainUIContextSaveObserver =
        [[NSNotificationCenter defaultCenter]
         addObserverForName:NSManagedObjectContextDidSaveNotification
         object:nil
         queue:nil
         usingBlock:^(NSNotification *note) {
             NSManagedObjectContext *savedContext = [note object];
             if (savedContext.parentContext == _mainUIManagedObjectContext) {
                 NSLog(@"AMBCoreData -> saving mainUIMOC");
                 [_mainUIManagedObjectContext performBlock:^{
                     NSError *error;
                     if (![_mainUIManagedObjectContext save:&error]) {
                         NSLog(@"AMBCoreData -> error saving mainUIMOC: %@ %@", [error localizedDescription], [error userInfo]);
                     }
                 }];
             }
         }];
    }
    return _mainUIManagedObjectContext;
}

+ (NSManagedObjectContext *)importManagedObjectContext
{
    if (!_importManagedObjectContext) {

        // Setup MOC attached to parent mainUIMOC in private queue
        _importManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
        [_importManagedObjectContext setParentContext:[self mainUIManagedObjectContext]];
    }
    return _importManagedObjectContext;
}

This code is pretty straightforward. I'm replicating the above diagram using only the mainUIManagedObjectContext in the NSMainQueueConcurrencyType. Every time the child context, importManagedObjectContext gets saved, a notification is fired and all the parent contexts performs a save in it's current thread.

I've implemented a test view controller with a UITableView and a NSFetchedResultsController attached. This is the code in the viewDidLoad of my test view controller:

- (void)viewDidLoad
{
    [super viewDidLoad];

    NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Task"];
    [request setSortDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"insertDate" ascending:NO]]];
    self.fetchRequest = request;
    NSFetchedResultsController *frc =
    [[NSFetchedResultsController alloc]
     initWithFetchRequest:self.fetchRequest
     managedObjectContext:[AMBCoreData mainUIManagedObjectContext]
     sectionNameKeyPath:nil
     cacheName:nil];
    frc.delegate = self;

    [self setFetchedResultsController:frc];
    [self.fetchedResultsController performFetch:nil];
}

Here I attach the mainUIManagedObjectContext to the NSFetchedResultsController. Later, in my viewDidAppear, I run a loop to insert a few Task entities:

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    [[AMBCoreData importManagedObjectContext] performBlock:^{
        for (int i = 0; i < 5000; i++) {
            Task *task = [NSEntityDescription insertNewObjectForEntityForName:@"Task" inManagedObjectContext:[AMBCoreData importManagedObjectContext]];
            task.title = [NSString stringWithFormat:@"Task %d", i];
            task.insertDate = [NSDate new];
        [[AMBCoreData importManagedObjectContext] save:nil];
    }];
}

The thing is, I'm inserting 5000 objects and the UI is freezing when the data is populated into the table view. Florian Kugler ran a test with this architecture inserting 15.000 objects and with instruments he got this main thread usage (blue is for main thread, gray for any other threads):

Main thread CPU usage with three-layer context architecture

But here's my main thread CPU usage with 5000 objects, profiled using an iPhone 5:

enter image description here

As you can see, my main thread usage is far greater than Florian's and also my UI freezes for a few seconds. My question is, am I doing something wrong? Is this the expected behaviour when using this three-layer MOC architecture with a NSFetchedResultsController and a UITableView? I know that inserting 5000 objects is not the usual behaviour of most apps, so when I've tried with 50 or 100 objects the freeze was inexistent or unnoticeable, but the main thread usage was high (although I admit that in this case it can be due another reasons like waking up the app).

like image 388
amb Avatar asked Jan 31 '14 09:01

amb


1 Answers

Yes, it is expected, because Main MOC is involved in the saves of its children. It is convenient and kind of okay when children of the UI context don’t do big saves, but often becomes a performance problem if those saves are bigger. You can’t be sure that the UI thread does only minimum job when using this pattern.

For the big saves I would recommend creating a context that is configured directly with the persistent store coordinator. After big save happens, you just refetch and optionally refresh data in the UI context. For more details see my answer here.

like image 114
eofster Avatar answered Sep 24 '22 20:09

eofster