Avoiding Faults in Core Data (Possible Bugs)

I work on an app that heavily relies on table views with NSFetchedResultsController.


I've found that on users' devices, the app lags. On the kind of large data set that some of our users have on their devices, Instruments showed that `tableView:heightForRowAtIndexPath:` was the biggest culprit (in it, we check the value of a property and return a different height based on that). According to Instruments, this process led to faults being fired constantly.


I'm trying to avoid this, I tried to use `returnsObjectsAsFaults = NO`, but that did nothing. Objects are still returned as faults. I'm guessing this is a bug, and I found references to it from years ago on Stack Overflow.


That was the first issue.


The second issue is that if I Cmd-click into the documentation for `NSFetchedRequest`, the comments on the `propertiesToFetch` property state the following:


"Specifies a collection of either NSPropertyDescriptions or NSString property names that should be fetched. The collection may represent attributes, to-one relationships, or NSExpressionDescription. If NSDictionaryResultType is set, the results of the fetch will be dictionaries containing key/value pairs where the key is the name of the specified property description. If NSManagedObjectResultType is set, then NSExpressionDescription cannot be used, and the results are managed object faults partially pre-populated with the named properties"


I tried it and this is indeed the behaviour. By adding the property that determines my table view row heights, I can avoid the fault firing. However, the actual documentation states:


"This value is only used if

resultType
is set to
NSDictionaryResultType
."


This is not the case.


These two issues seem like bugs to me. But I'm posting this because Core Data is like quantum mechanics - anyone who says they know it, doesn't know it - and I'm wondering if I'm missing something here.

Core data has a lot of interrelated pieces, which means that sometimes value settings interfer with others.


It would be more helpful if you were to post some code examples, along with what you expect to happen and what is actually happening.

The code would be this:


NSFetchRequest* request = [NSFetchRequest fetchRequestForEntityName:@"MyEntity"];
request.sortDescriptors = ...
request.returnsObjectsAsFaults = NO;

// Create NSFetchedResultsController with the request, performFetch:, then in the table view controller delegate:
-(CGFloat)tableView:(UITableView*)tableView heightForRowAtIndexPath:(NSIndexPath*)indexPath {
     NSManagedObject* object = [self.fetchedResultsController objectAtIndexPath:indexPath];
     // [object isFault] should return NO, instead returns YES.
     // attempt to access any properties on object fires the fault
}



Instead, I found that I can get the desired effect with the following:


NSFetchRequest* request = [NSFetchRequest fetchRequestForEntityName:@"MyEntity"];
request.sortDescriptors = ...

request.propertiesToFetch = @[ /* An array of NSPropertyDescription objects */ ];

// Create NSFetchedResultsController with the request, performFetch:, then in the table view controller delegate:
-(CGFloat)tableView:(UITableView*)tableView heightForRowAtIndexPath:(NSIndexPath*)indexPath {
     NSManagedObject* object = [self.fetchedResultsController objectAtIndexPath:indexPath];
     // [object isFault] still returns YES, but the properties that were included in `propertiesToFetch` can be accessed without firing the fault.
}



Both of these behaviours are against the official documentation.

As of iOS 8 you don't need to implement heightForRowAtIndexPath:. You should implement self-sizing cells.


Also, it is well known that variable height cells and large numbers of rows results in slow table views.

We still support iOS 7 and have users on it (although probably not for much longer).


Also, that everyone knows "variable height cells = slow table views" is not helpful. In this case, I've measured exactly what's slow about it (faults being fired) and I'm trying to alleviate that. Core Data has a mechanism built specifically to address the problem I'm seeing. I need to get it to work.

I had the same problem a few years ago, that is dealing with measuring variable row heights for data in Core Data. What I ended up doing was a bit of a hack, but I created an offscreen text view that I used to measure each piece of text as it was being stored in Core Data. The measurement value was stored with the record, then regurgitated in heightForRowAtIndexPath:. I never had problems with faulting, but once I implemented pre-measuring the text, table scrolling was smooth as butter.

Well... I have solved my performance issue now: It turned out it was much dumber/simpler than any of the stuff we're talking about... `tableView:estimatedHeightForRowAtIndexPath:` was not implemented!!!


That simple fix was the difference between faults firing (one by one) for 3000 objects vs 8 objects!


Having said that, I'd still love to hear something decisive about the function of `propertiesToFetch` and `returnsObjectsAsFaults`. They can definitely be useful in the right place.

Did you see the Core Data video at this year's WWDC?


Even though the individual attribute pre-fetching may not have officially worked before, it's one of the new features officially as of iOS 9. I wouldn't be surprised if the documentation was behind schedule compared to the implementation, though.


Likewise, as far as faulting goes, if your memory footprint is large enough, CoreData is going to ignore what you specify for the faulting behavior and not fault your objects into memory. Or, rather, the faulting attempt will simply silently fail instead of crashing the application.

Hmm... yeah I did watch it. Don't remember anything about this... my brain has poor retention of WWDC videos :/


Thanks for the info re. Core Data ignoring specified faulting behavior. I've read the Core Data guide, but haven't seen any references to this. Do you have any links?

FRC has its own "special rules" for dealing with the MOC is manages.


This is not the "fault" of core data, per se. This is the result of how NSFetchedResultsControllrer interacts with the fetch.


You can clearly see the difference between what happens when you fetch via "raw" core data and "fetching" with FRC if you instrument the calls (you will find the results in a file named /tmp/msgSends-XXXX).


Create a MOC, then fetch...


    instrumentObjcMessageSends(YES);
    [moc executeFetchRequest:fetchRequest error:NULL]
    instrumentObjcMessageSends(NO);


Create a FRC, then fetch...


    instrumentObjcMessageSends(YES);
    [frc performFetch:NULL];
    instrumentObjcMessageSends(NO);


Or, you can just look at the results that you get back, and you can see that when you perform the fetch on the MOC, its returned array honors your returnsObjectsAsFaults setting, but when using the FRC, it uses its own heuristics for managing the objects in its fetched results array. Remember, FRC does not return an array. It gives you access to the array it manages internally.


FRC has a history of aggressively managing its objects.


If you consider this behavior to be a bug, you should file a bug report against FRC.

This was very helpfu. Thanks, Jody.

Avoiding Faults in Core Data (Possible Bugs)
 
 
Q