Search code examples
iosobjective-cuitableviewcore-datansfetchedresultscontroller

Core Data sectionNameKeyPath with Relationship Attribute Performance Issue


I have a Core Data Model with three entities:
Person, Group, Photo with relationships between them as follows:

  • Person <<-----------> Group (one to many relationship)
  • Person <-------------> Photo (one to one)

When I perform a fetch using the NSFetchedResultsController in a UITableView, I want to group in sections the Person objects using the Group's entity name attribute.

For that, I use sectionNameKeyPath:@"group.name".

The problem is that when I'm using the attribute from the Group relationship, the NSFetchedResultsController fetches everything upfront in small batches of 20 (I have setFetchBatchSize: 20) instead of fetching batches while I'm scrolling the tableView.

If I use an attribute from the Person entity (like sectionNameKeyPath:@"name") to create sections everything works OK: the NSFetchResultsController loads small batches of 20 objects as I scroll.

The code I use to instantiate the NSFetchedResultsController:

- (NSFetchedResultsController *)fetchedResultsController {

    if (_fetchedResultsController) {
        return _fetchedResultsController;
    }

    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
    NSEntityDescription *entity = [NSEntityDescription entityForName:[Person description]
                                              inManagedObjectContext:self.managedObjectContext];

    [fetchRequest setEntity:entity];

    // Specify how the fetched objects should be sorted
    NSSortDescriptor *groupSortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"group.name"
                                                                        ascending:YES];

    NSSortDescriptor *personSortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"birthName"
                                                                         ascending:YES
                                                                          selector:@selector(localizedStandardCompare:)];


    [fetchRequest setSortDescriptors:[NSArray arrayWithObjects:groupSortDescriptor, personSortDescriptor, nil]];

    [fetchRequest setRelationshipKeyPathsForPrefetching:@[@"group", @"photo"]];
    [fetchRequest setFetchBatchSize:20];

    NSError *error = nil;
    NSArray *fetchedObjects = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];

    if (fetchedObjects == nil) {
        NSLog(@"Error Fetching: %@", error);
    }

    _fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
                                                                    managedObjectContext:self.managedObjectContext sectionNameKeyPath:@"group.name" cacheName:@"masterCache"];

    _fetchedResultsController.delegate = self;

    return _fetchedResultsController;
}

This is what I get in Instruments if I create sections based on "group.name" without any interaction with the App's UI: Core Data Fetch with Sections by Relationship

And this is what I get (with a bit of scrolling on UITableView) if sectionNameKeyPath is nil: Core Data Fetch without any Sections

Please, can anyone help me out on this issue?

EDIT 1:

It seems that I get inconsistent results from the simulator and Instruments: when I've asked this question, the app was starting in the simulator in about 10 seconds (by Time Profiler) using the above code.

But today, using the same code as above, the app starts in the simulator in 900ms even if it makes a temporary upfront fetch for all the objects and it's not blocking the UI.

I've attached some fresh screenshots: Time Profiler with Simulator Upfront Fetch in Simulator without scrolling Upfront Fetch in Simulator with scrolling and small batch fetches

EDIT 2: I reset the simulator and the results are intriguing: after performing an import operation and quitting the app the first run looked like this: First run after simulator reset and new import After a bit of scrolling: First run after simulator reset, new import and some scrolling Now this is what happens on a second run: Second run after simulator reset and new import After the fifth run: Fifth run

EDIT 3: Running the app the seventh time and eight time, I get this: Seventh run Eighth run


Solution

  • After almost a year since I've posted this question, I've finally found the culprits that enable this behaviour (which slightly changed in Xcode 6):

    1. Regarding the inconsistent fetch times: I was using a cache and at the time I was back and forth with opening, closing and resetting the simulator.

    2. Regarding the fact that everything was fetched upfront in small batches without scrolling (in Xcode 6's Core Data Instruments that's not the case anymore - now it's one, big fetch which takes entire seconds):

    It seems that setFetchBatchSize does not work correctly with parent/child contexts. The issue was reported back in 2012 and it seems that it's still there http://openradar.appspot.com/11235622.

    To overcome this issue, I created another independent context with an NSMainQueueConcurrencyType and set its persistence coordinator to be the same that my other contexts are using.

    More about issue #2 here: https://stackoverflow.com/a/11470560/1641848