Tuning Performance, Memory Usage, and Multithreading - Pro iOS Persistence: Using Core Data (2014)

Pro iOS Persistence: Using Core Data (2014)

Chapter 9. Tuning Performance, Memory Usage, and Multithreading

People want answers. Now. They expect their devices to provide those answers quickly. If they believe the applications on their devices are locked up or even just slow, they’ll abandon or perhaps even delete them. You can show spinners to indicate that your applications are working hard on providing those results, which will mollify users somewhat. Giving a visual clue that the device is working beats giving no such clue and letting customers think their devices have locked up; better still would be to never have episodes of slowness that make users wait. You may not always be able to achieve that goal, but you should always try.

Fetching and storing data in a persistent store can take a long time, especially when mounds of data are involved. This chapter will make sure that you understand how Core Data helps you—and how you must help yourself—to ensure that you don’t pepper your users with “Please Wait” spinners or, worse, an unresponsive application that appears locked up while it retrieves or saves large object graphs. You will learn how to utilize the various tools and strategies, such as caching, faulting, multiple threads, and the Instruments application that comes with Xcode.

Tuning Performance

The phrase “tuning performance” conjures an image of a mechanic with a set of wrenches, huddled over an engine on his workbench, turning bolts and tweaking valves and measuring the effects of his adjustments on the performance of the engine. When tuning the performance of your applications, you must have the same discipline to make adjustments and measure their effects on overall performance. In this chapter, we cover various approaches to improving the performance of your applications. You should try out these approaches on the applications you write and measure their effects, both in the simulator and on actual iDevices.

Building the Application for Testing

You need a way to perform all this performance testing so that you can verify results. This section walks you through building an application that will allow you to run various tests and see the results. The application, shown in Figure 9-1, presents a list of tests you can run in a standard picker view. To run a test, select it in the picker, click the Run Selected Test button, and wait for the results. After the test runs, you’ll see the start time, the stop time, the number of elapsed seconds for the test, and some text describing the results of the test.

image

Figure 9-1. The PerformanceTuning application

Creating the Project

In Xcode, create a new single-view application for iPhone called PerformanceTuning with the User Core Data check box unchecked. Add a Core Data data model called PerformanceTuning.xcdatamodeld and a class called Persistence to manage the Core Data stack. Listings 9-1 and 9-2 show Persistence.h and Persistence.m, respectively. Listing 9-3 shows Persistence.swift.

Listing 9-1. Persistence.h

@import CoreData;

@interface Persistence : NSObject

@property (readonly, strong, nonatomic) NSManagedObjectContext *managedObjectContext;
@property (readonly, strong, nonatomic) NSManagedObjectModel *managedObjectModel;
@property (readonly, strong, nonatomic) NSPersistentStoreCoordinator *persistentStoreCoordinator;

- (void)saveContext;
- (NSURL *)applicationDocumentsDirectory;

@end

Listing 9-2. Persistence.m

#import "Persistence.h"

@implementation Persistence

- (instancetype)init {
self = [super init];
if (self != nil) {
// Initialize the managed object model
NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"PerformanceTuning" withExtension:@"momd"];
_managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];

// Initialize the persistent store coordinator
NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"PerformanceTuning.sqlite"];

NSError *error = nil;
_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.managedObjectModel];
if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil
URL:storeURL
options:nil
error:&error]) {
NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
abort();
}

// Initialize the managed object context
_managedObjectContext = [[NSManagedObjectContext alloc] init];
[_managedObjectContext setPersistentStoreCoordinator:self.persistentStoreCoordinator];
}
return self;
}

#pragma mark - Helper Methods

- (void)saveContext {
NSError *error = nil;
if ([self.managedObjectContext hasChanges] && ![self.managedObjectContext save:&error]) {
NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
abort();
}
}

- (NSURL *)applicationDocumentsDirectory {
return [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
}

@end

Listing 9-3. Persistence.swift

import Foundation
import CoreData

class Persistence {
// MARK: - Core Data stack

lazy var applicationDocumentsDirectory: NSURL = {
// The directory the application uses to store the Core Data store file. This code uses a directory named "book.persistence.PerformanceTuningSwift" in the application's documents Application Support directory.
let urls = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)
return urls[urls.count-1] as NSURL
}()

lazy var managedObjectModel: NSManagedObjectModel = {
// The managed object model for the application. This property is not optional. It is a fatal error for the application not to be able to find and load its model.
let modelURL = NSBundle.mainBundle().URLForResource("PerformanceTuningSwift", withExtension: "momd")!
return NSManagedObjectModel(contentsOfURL: modelURL)!
}()

lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator? = {
// The persistent store coordinator for the application. This implementation creates and return a coordinator, having added the store for the application to it. This property is optional since there are legitimate error conditions that could cause the creation of the store to fail.
// Create the coordinator and store
var coordinator: NSPersistentStoreCoordinator? = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel)
let url = self.applicationDocumentsDirectory.URLByAppendingPathComponent("PerformanceTuningSwift.sqlite")
var error: NSError? = nil
var failureReason = "There was an error creating or loading the application's saved data."
if coordinator!.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: url, options: nil, error: &error) == nil {
coordinator = nil
// Report any error we got.
let dict = NSMutableDictionary()
dict[NSLocalizedDescriptionKey] = "Failed to initialize the application's saved data"
dict[NSLocalizedFailureReasonErrorKey] = failureReason
dict[NSUnderlyingErrorKey] = error
error = NSError(domain: "YOUR_ERROR_DOMAIN", code: 9999, userInfo: dict)
// Replace this with code to handle the error appropriately.
// abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
NSLog("Unresolved error \(error), \(error!.userInfo)")
abort()
}

return coordinator
}()

lazy var managedObjectContext: NSManagedObjectContext? = {
// Returns the managed object context for the application (which is already bound to the persistent store coordinator for the application.) This property is optional since there are legitimate error conditions that could cause the creation of the context to fail.
let coordinator = self.persistentStoreCoordinator
if coordinator == nil {
return nil
}
var managedObjectContext = NSManagedObjectContext()
managedObjectContext.persistentStoreCoordinator = coordinator
return managedObjectContext
}()

// MARK: - Core Data Saving support

func saveContext () {
if let moc = self.managedObjectContext {
var error: NSError? = nil
if moc.hasChanges && !moc.save(&error) {
// Replace this implementation with code to handle the error appropriately.
// abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
NSLog("Unresolved error \(error), \(error!.userInfo)")
abort()
}
}
}
}

For the Objective-C version, add a Persistence property to AppDelegate.h, as shown in Listing 9-4. For both Swift and Objective-C, initialize the Persistence property in AppDelegate’s application:didFinishLaunchingWithOptions: method, as shown inListing 9-5 (Objective-C) or Listing 9-6 (Swift). Make sure to import Persistence.h in AppDelegate.m if you’re doing this in Objective-C

Listing 9-4. AppDelegate.h with the Persistence Property

#import <UIKit/UIKit.h>

@class Persistence;

@interface AppDelegate : UIResponder <UIApplicationDelegate>

@property (strong, nonatomic) UIWindow *window;
@property (strong, nonatomic) Persistence *persistence;

@end

Listing 9-5. Initializing the Core Data Stack in AppDelegate.m (Objective-C)

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.persistence = [[Persistence alloc] init];
return YES;
}

Listing 9-6. Initializing the Core Data Stack in AppDelegate.swift (Swift)

var persistence: Persistence?

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
self.persistence = Persistence()
return true
}

Creating the Data Model and Data

The data model for this application tracks people and selfies (photographs taken of oneself, optionally including other people) across social networks and consists of three entities: Person, Selfie, and SocialNetwork. Each person can appear in multiple selfies, and each selfie can tag multiple people. Also, each selfie can be posted to multiple social networks, and each social network can display multiple selfies. This means that Person and Selfie have an optional many-to-many relationship, and Selfie and SocialNetwork have an optional many-to-many relationship.

We also want some attributes on each entity, and to simplify the model we’ll give each a name attribute of the type String and a rating attribute of the type Integer 16. Go ahead and create this model, which should match Figure 9-2.

image

Figure 9-2. The Core Data model

We’ll next push enough data into the data model so we can run performance tests and differentiate results. It should be sufficient to create 500 people, selfies, and social networks and relate them all to each other—that will give us three tables with 500 rows each (Person, Selfie, andSocialNetwork) and two join tables with 250,000 rows each (Person-to-Selfie and Selfie-to-SocialNetwork).

To insert the data, open Persistence.h (for Objective-C) and declare a method to load the data.

- (void)loadData;

Implement the method in Persistence.m or Persistence.swift. The implementation should check the persistent store to determine whether the data have already been loaded so that subsequent invocations of the program don’t compound the data store. If the data have not been inserted, the loadData method creates 500 people with names like Person 1, Person 2, and so on; 500 selfies with names like Selfie 1; and 500 social networks with names like SocialNetwork 1. It uses a helper method called insertObjectForName:index: to actually create the object. After creating all the objects, the code loops through all the selfies and adds relationships to all the people and all the social networks. Finally, loadData saves the object graph to the persistent data store. Listing 9-7 shows the Objective-C code, and Listing 9-8shows the Swift code.

Listing 9-7. Loading the Data in Persistence.m (Objective-C)

- (void)loadData {
static int NumberOfRows = 500;

// Fetch the people. If we have NumberOfRows, assume our data is loaded.
NSFetchRequest *peopleFetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Person"];
NSArray *people = [self.managedObjectContext executeFetchRequest:peopleFetchRequest
error:nil];
if ([people count] != NumberOfRows) {
NSLog(@"Creating objects");
// Load the objects
for (int i = 1; i <= NumberOfRows; i++) {
[self insertObjectForName:@"Person" index:i];
[self insertObjectForName:@"Selfie" index:i];
[self insertObjectForName:@"SocialNetwork" index:i];
}

// Get all the people, selfies, and social networks
people = [self.managedObjectContext executeFetchRequest:peopleFetchRequest
error:nil];

NSFetchRequest *selfiesFetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Selfie"];
NSArray *selfies = [self.managedObjectContext executeFetchRequest:selfiesFetchRequest
error:nil];

NSFetchRequest *socialNetworksFetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"SocialNetwork"];
NSArray *socialNetworks = [self.managedObjectContext executeFetchRequest:socialNetworksFetchRequest error:nil];

// Set up all the relationships
NSLog(@"Creating relationships");
for (NSManagedObject *selfie in selfies) {
NSMutableSet *peopleSet = [selfie mutableSetValueForKey:@"people"];
[peopleSet addObjectsFromArray:people];

NSMutableSet *socialNetworksSet = [selfie mutableSetValueForKey:@"socialNetworks"];
[socialNetworksSet addObjectsFromArray:socialNetworks];
}

[self saveContext];
}
}

- (NSManagedObject *)insertObjectForName:(NSString *)name index:(NSInteger)index {
NSManagedObject *object = [NSEntityDescription insertNewObjectForEntityForName:name
inManagedObjectContext:self.managedObjectContext];
[object setValue:[NSString stringWithFormat:@"%@ %ld", name, (long)index]
forKey:@"name"];
[object setValue:[NSNumber numberWithInt:((arc4random() % 10) + 1)]
forKey:@"rating"];
return object;
}

Listing 9-8. Loading the Data in Persistence.swift (Swift)

func loadData() {
let NumberOfRows = 500

// Fetch the people. If we have NumberOfRows, assume our data is loaded.
let peopleFetchRequest = NSFetchRequest(entityName: "Person")
var people = self.managedObjectContext?.executeFetchRequest(peopleFetchRequest, error: nil)
if people?.count != NumberOfRows {
println("Creating objects")
// Load the objects
for var i=1; i<=NumberOfRows; i++ {
self.insertObjectForName("Person", index: i)
self.insertObjectForName("Selfie", index: i)
self.insertObjectForName("SocialNetwork", index: i)
}

// Get all the people, selfies, and social networks
people = self.managedObjectContext?.executeFetchRequest(peopleFetchRequest, error: nil)

let selfiesFetchRequest = NSFetchRequest(entityName: "Selfie")
var selfies = self.managedObjectContext?.executeFetchRequest(selfiesFetchRequest, error: nil)

let socialNetworksFetchRequest = NSFetchRequest(entityName: "SocialNetwork")
var socialNetworks = self.managedObjectContext?.executeFetchRequest(socialNetworksFetchRequest, error: nil)

// Set up all the relationships
println("Creating relationships")
for selfie in selfies as [NSManagedObject] {
var peopleSet = selfie.mutableSetValueForKey("people")
peopleSet.addObjectsFromArray(people!)

var socialNetworksSet = selfie.mutableSetValueForKey("socialNetworks")
socialNetworksSet.addObjectsFromArray(socialNetworks!)
}

self.saveContext()
}
}

func insertObjectForName(name: String, index: Int) -> NSManagedObject! {
var object = NSEntityDescription.insertNewObjectForEntityForName(name, inManagedObjectContext: self.managedObjectContext!) as NSManagedObject

object.setValue(NSString(format: "%@ %ld", name, index), forKey: "name")
object.setValue(NSNumber(int: Int(arc4random() % 10) + 1), forKey: "rating")

return object
}

Each time the application launches, we call loadData to load if the data are necessary. Add this call in AppDelegate.m or AppDelegate.swift, as shown in Listing 9-9 (Objective-C) or Listing 9-10 (Swift).

Listing 9-9. Calling loadData in AppDelegate.m (Objective-C)

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.persistence = [[Persistence alloc] init];
[self.persistence loadData];
return YES;
}

Listing 9-10. Calling loadData in AppDelegate.swift (Swift)

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Override point for customization after application launch.
self.persistence = Persistence()
self.persistence?.loadData()
return true
}

Build and run the program to create your persistent data store. You should see the log messages in the Xcode console that your program is creating the objects and relationships. If you quit the program and run it again, you shouldn’t see any such log messages.

Creating the Testing View

The PerformanceTuning application presents a picker with the list of tests you can run and buttons to launch the selected test. When you run a test, the application lists the start time, stop time, and elapsed time for the test, as well as a description of the test’s results. OpenViewController.h, declare that it implements the protocols for your picker, add properties for the user interface (UI) elements, and add a method declaration for running a test, as Listing 9-11 shows.

Listing 9-11. ViewController.h

@interface ViewController : UIViewController <UIPickerViewDataSource, UIPickerViewDelegate>

@property (nonatomic, weak) IBOutlet UILabel *startTime;
@property (nonatomic, weak) IBOutlet UILabel *stopTime;
@property (nonatomic, weak) IBOutlet UILabel *elapsedTime;
@property (nonatomic, weak) IBOutlet UITextView *results;
@property (nonatomic, weak) IBOutlet UIPickerView *testPicker;

- (IBAction)runTest:(id)sender;

@end

Open ViewController.m and add stub implementations for the picker view protocols and the runTest: method, as Listing 9-12 shows.

Listing 9-12. ViewController.m

#import "ViewController.h"

@implementation ViewController

#pragma mark - UIPickerViewDataSource methods

- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView {
return 0;
}

- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component {
return 0;
}

#pragma mark - UIPickerViewDelegate methods

- (NSString *)pickerView:(UIPickerView *)pickerView titleForRow:(NSInteger)row forComponent:(NSInteger)component {
return nil;
}

#pragma mark - Handlers

- (IBAction)runTest:(id)sender {
}

@end

If you’re using Swift, make the corresponding changes, shown in Listing 9-13, in ViewController.swift.

Listing 9-13. ViewController.swift

class ViewController: UIViewController, UIPickerViewDelegate, UIPickerViewDataSource {

@IBOutlet weak var startTime: UILabel!
@IBOutlet weak var stopTime: UILabel!
@IBOutlet weak var elapsedTime: UILabel!
@IBOutlet weak var results: UITextView!
@IBOutlet weak var testPicker: UIPickerView!

...

func numberOfComponentsInPickerView(pickerView: UIPickerView) -> Int {
return 1
}

func pickerView(pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return self.tests.count
}

func pickerView(pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String! {
let test = self.tests[row]
let fullName = _stdlib_getDemangledTypeName(test)
let tokens = split(fullName, { $0 == "." })
return tokens.last
}

@IBAction func runTest(sender: AnyObject) {
}
}

Open Main.storyboard and build the UI. Add labels for Start Time, Stop Time, and Elapsed Time, as well as right-aligned labels with Autoshrink set to “Minimum Font Size” of 9, to show the actual start time, stop time, and elapsed time when running a test. Add a text view to display the results of a test, a button for launching a test, and finally a picker view to show the available tests. Drag all those items to the view and arrange them to mimic Figure 9-3. Connect the view’s controls to the corresponding properties in the code and the button to the runTest: method. Also, connect the picker view’s data source and delegate to the view controller.

image

Figure 9-3. Building the view for PerformanceTuning

Building the Testing Framework

The picker view will contain a list of tests that we can run. To create that list, we create a protocol called PerfTest and then create several classes that implement that protocol. Then, we’ll create an array to store instances of our test classes. The application will display the names of those test classes in the picker view and execute the selected test when the user taps the Run Selected Test button.

To begin, create a new protocol called PerfTest and declare a required method called runTestWithContext:. Listing 9-14 shows the Objective-C version, PerfTest.h, and Listing 9-15 shows the Swift version, PerfTest.swift.

Listing 9-14. PerfTest.h

#import <Foundation/Foundation.h>
@import CoreData;

@protocol PerfTest <NSObject>

- (NSString *)runTestWithContext:(NSManagedObjectContext *)context;

@end

Listing 9-15. PerfTest.swift

import Foundation
import CoreData

protocol PerfTest {
func runTestWithContext(context: NSManagedObjectContext) -> String!
}

Before integrating the testing framework into the application, create a test—a “test” test, if you will—so that you have something both to see in the picker view and to run. The first test you create will fetch all the objects—the people, selfies, and social networks—from the persistent store. Create a new class called FetchAll as a subclass of NSObject, and make it implement the PerfTest protocol. Then, in the implementation of runWithContext:, simply fetch all the objects and return a string describing the number of objects of each entity fetched. Listing 9-16 showsFetchAll.h, Listing 9-17 shows FetchAll.m, and Listing 9-18 shows FetchAll.swift.

Listing 9-16. FetchAll.h

#import "PerfTest.h"

@interface FetchAll : NSObject <PerfTest>

@end

Listing 9-17. FetchAll.m

#import "FetchAll.h"

@implementation FetchAll

- (NSString *)runTestWithContext:(NSManagedObjectContext *)context {
NSFetchRequest *peopleFetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Person"];
NSFetchRequest *selfiesFetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Selfie"];
NSFetchRequest *socialNetworksFetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"SocialNetwork"];

NSArray *people = [context executeFetchRequest:peopleFetchRequest
error:nil];
NSArray *selfies = [context executeFetchRequest:selfiesFetchRequest
error:nil];
NSArray *socialNetworks = [context executeFetchRequest:socialNetworksFetchRequest
error:nil];

return [NSString stringWithFormat:@"Fetched %ld people, %ld selfies, and %ld social networks", [people count], [selfies count], [socialNetworks count]];
}

@end

Listing 9-18. FetchAll.swift

import Foundation
import CoreData

class FetchAll : PerfTest {
func runTestWithContext(context: NSManagedObjectContext) -> String! {
let peopleFetchRequest = NSFetchRequest(entityName: "Person")
let selfiesFetchRequest = NSFetchRequest(entityName: "Selfie")
let socialNetworkFetchRequest = NSFetchRequest(entityName: "SocialNetwork")

let people = context.executeFetchRequest(peopleFetchRequest, error: nil)!
let selfies = context.executeFetchRequest(selfiesFetchRequest, error: nil)!
let socialNetworks = context.executeFetchRequest(socialNetworkFetchRequest, error: nil)!

return NSString(format: "Fetched %ld people, %ld selfies, and %ld social networks", people.count, selfies.count, socialNetworks.count)
}
}

Adding the Testing Framework to the Application

Now you must modify the view controller to use the testing framework. Add a property to ViewController.h to hold all the test instances.

@property (nonatomic, strong) NSArray *tests; // Objective-C
var tests = [PerfTest]() // Swift

In ViewController.m or ViewController.swift, you’ll do the following:

· Add necessary imports.

· Implement viewDidLoad to blank out fields and create your tests array.

· Update the UIPickerViewDataSource and UIPickerViewDelegate methods to populate the picker view.

· Update the runTest: method to run the selected test.

Listing 9-19 shows the updated ViewController.m file, and Listing 9-20 shows the updated ViewController.swift file. You can see that in the runTest: method, we determine which test is selected in the picker view. We grab a pointer to the application’s managed object context and we reset it so our test times are consistent. We mark the start time, run the selected test, and then mark the stop time. Finally, we update the UI to show the times and results.

Listing 9-19. ViewController.m Updated with the Testing Framework

#import "ViewController.h"
#import "FetchAll.h"
#import "AppDelegate.h"
#import "Persistence.h"

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];

self.startTime.text = @"";
self.stopTime.text = @"";
self.elapsedTime.text = @"";
self.results.text = @"";

self.tests = @[[[FetchAll alloc] init]];
}

#pragma mark - UIPickerViewDataSource methods

- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView {
return 1;
}

- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component {
return [self.tests count];
}

#pragma mark - UIPickerViewDelegate methods

- (NSString *)pickerView:(UIPickerView *)pickerView titleForRow:(NSInteger)row forComponent:(NSInteger)component {
id <PerfTest> test = self.tests[row];
return [[test class] description];
}

#pragma mark - Handlers

- (IBAction)runTest:(id)sender {
// Get the selected test
id <PerfTest> test = self.tests[[self.testPicker selectedRowInComponent:0]];
AppDelegate *delegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
NSManagedObjectContext *context = delegate.persistence.managedObjectContext;

// Clear out any objects so we get clean test results
[context reset];

// Mark the start time, run the test, and mark the stop time
NSDate *start = [NSDate date];
NSString *results = [test runTestWithContext:delegate.persistence.managedObjectContext];
NSDate *stop = [NSDate date];

// Update the UI with the test results
self.startTime.text = [start description];
self.stopTime.text = [stop description];
self.elapsedTime.text = [NSString stringWithFormat:@"%.03f seconds", [stop timeIntervalSinceDate:start]];
self.results.text = results;
}

@end

Listing 9-20. ViewController.swift Updated with the Testing Framework

import UIKit

class ViewController: UIViewController, UIPickerViewDelegate, UIPickerViewDataSource {

@IBOutlet weak var startTime: UILabel!
@IBOutlet weak var stopTime: UILabel!
@IBOutlet weak var elapsedTime: UILabel!
@IBOutlet weak var results: UITextView!
@IBOutlet weak var testPicker: UIPickerView!

var tests = [PerfTest]()

func numberOfComponentsInPickerView(pickerView: UIPickerView) -> Int {
return 1
}

func pickerView(pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return self.tests.count
}

func pickerView(pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String! {
let test = self.tests[row]
let fullName = _stdlib_getDemangledTypeName(test)
let tokens = split(fullName, { $0 == "." })
return tokens.last
}

@IBAction func runTest(sender: AnyObject) {
// Get the selected test
let test = self.tests[self.testPicker.selectedRowInComponent(0)]

let appDelegate = UIApplication.sharedApplication().delegate as AppDelegate
let context = appDelegate.persistence?.managedObjectContext
if let context = context {
// Clear out any objects so we get clean test results
context.reset()

// Mark the start time, run the test, and mark the stop time
let start = NSDate()
let testResults = test.runTestWithContext(context)
let stop = NSDate()

let formatter = NSDateFormatter()
formatter.timeStyle = .MediumStyle

// Update the UI with the test results
self.startTime.text = formatter.stringFromDate(start)
self.stopTime.text = formatter.stringFromDate(stop)
self.elapsedTime.text = NSString(format: "%.03f seconds", stop.timeIntervalSinceDate(start))
self.results.text = testResults
}
}

override func viewDidLoad() {
super.viewDidLoad()

self.startTime.text = ""
self.stopTime.text = ""
self.elapsedTime.text = ""
self.results.text = ""

self.tests.append(FetchAll())
}
}

You have completed the application—for now. As you work through this chapter, you will add tests to cover the scenarios you read about.

Running Your First Test

Build and run the application. When you run the application, you should see the iOS Simulator with a single test, “FetchAll,” in the picker view. If the picker view is blank, you likely neglected to connect the picker view’s dataSource and delegate properties to the view controller. Once you have everything working, and with “FetchAll” selected, tap on Run Selected Test. The application will fetch all the objects and display the results, as shown in Figure 9-4.

image

Figure 9-4. The results of fetching all the objects

The rest of this chapter explains the various performance considerations imposed by Core Data on iOS and the devices it runs on. As you learn about these performance considerations, you’ll add tests to the PerformanceTuning application. Feel free to add your own tests as well so that you always give the users of your applications the best possible experience.

Faulting

When you run SQL queries against a database, you know the depth and range of the data you’re pulling. You know if you’re joining tables, you know the columns you’re pulling back, and, if you’re careful, you can limit the number of rows you yank from the database into working memory. You may have to work harder to get data than you do with Core Data, but you have more control. You know about how much memory you’re using to hold the data you fetch.

With Core Data, however, you’ve given up some amount of control in exchange for ease of use. In the PerformanceTuning model, for example, each Selfie instance has a relationship to a collection of Person instances, which you access in an object-oriented, not a data-oriented, way. You don’t have to care whether Core Data pulls the Person data from the database when the application starts, when the Selfie loads, or when you first access the Person objects. Core Data cares, however, and attempts to delay loading data from the persistent store into the object graph until necessary, reducing both fetch time and memory usage. It does this using what’s termed “faults.”

Think of a fault like a symlink on a Unix file system, which is not the actual file it points to nor does it contain the file’s data. The symlink represents the file, though, and when called upon can get to the file and its data, just as if it were the file itself. Like a symlink, a fault is a placeholder that can get to the persistent store’s data. A fault can represent either a managed object or, when it represents a to-many relationship, a collection of managed objects. As a link to or a shadow of the data, a fault occupies much less memory than the actual data do. See Figure 9-5 for a depiction of a managed object and a fault. In this case, the managed object is an instance of Selfie, and the fault points to the related collection of Person objects. The Selfie is boldly depicted, representing the fact that it has been fetched from the persistent store and resides in memory. ThePerson objects, represented on the “to-many” side of the relationship, are faint and grayed out, because they haven’t been fetched from the persistent store and do not reside in memory.

image

Figure 9-5. A selfie and its faulted people

Firing Faults

Core Data uses the term “firing faults” when it must pull the data from the persistent store that a fault points to and then put that data into memory. Core Data has “fired” off a request to fetch data from the data store, and the fault has been “fired” from its job to represent the actual data. You cause a fault to fire any time you request a managed object’s persistent data, whether through valueForKey: or through methods of a custom class that either return or access the object’s persistent data. Methods that access a managed object’s metadata and not any of the data stored in the persistent store don’t fire a fault. This means you can query a managed object’s class, hash, description, entity, and object ID, among other things, and not cause a fault to fire. For the complete list of which methods don’t fire a fault, see Apple’s documentation athttp://developer.apple.com/library/ios/#documentation/Cocoa/Conceptual/CoreData/Articles/cdPerformance.html under the section “Faulting Behavior.”

Explicitly asking a managed object for its persistent data causes a fault to fire, but so does calling any methods or constructs that access persistent data from a managed object. You can, for example, access a collection through a managed object’s relationships without causing a fault to fire. Sorting the collection, however, will fire a fault because any logical sorting will sort by some of the object’s persistent data. The test you build later in this section demonstrates this behavior.

Faulting and Caching

We fibbed a bit when we said that firing faults fetches data from the persistent store. That’s often true, but before Core Data treks all the way to the persistent store to retrieve data, it first checks its cache for the data it seeks. If Core Data finds its intended data in the cache, it pulls the data from cache and skips the longer trip to the persistent store. The sections “Caching” and “Expiring the Cache” discuss caching in more detail.

Refaulting

One way to take control of your application’s persistent data and memory use is to turn managed objects back into faults, thereby relinquishing the memory their data were occupying. You turn objects back into faults by calling the managed object context’srefreshObject:mergeChanges: method, passing the object you want to fault and NO for the mergeChanges: parameter. If, for example, I had a managed object called foo that I wanted to turn back into a fault, I would use code similar to the following:

[context refreshObject:foo mergeChanges:NO]; // Objective-C
context.refreshObject(foo, mergeChanges:false) // Swift

After this call, foo is now a fault, and [foo isFault] (or foo.fault for Swift) returns YES (or true for Swift). Understanding the mergeChanges parameter is important. Passing NO or false, as the previous code does, throws away any changed data that has not yet been saved to the persistent store. Take care when doing this because you lose all data changes in this object you’re faulting, and all the objects to which the faulted object relates are released. If any of the relationships have changed and the context is then saved, your faulted object is out of sync with its relationships, and you have created data integrity issues in your persistent store.

Passing YES or true for mergeChanges doesn’t fault the object. Instead, it reloads all the object’s persistent data from the persistent store (or the last cached state) and then reapplies any changes that existed before the call to refreshObject:mergeChanges: that have not yet been saved.

When you turn a managed object into a fault, Core Data sends two messages to the managed object.

· willTurnIntoFault: before the object faults

· didTurnIntoFault: after the object faults

If you have implemented custom classes for your managed objects, you can implement either or both of these methods in your classes to perform some action on the object. Suppose, for example, that your custom managed object performs an expensive calculation on some persistent values and caches the result, and you want to nullify that cached result if the values it depends on aren’t present. You could nullify the calculation in didTurnIntoFault:.

Building the Faulting Test

To test what you’ve learned about faulting in this section, build a test that will do the following:

1. Retrieve the first selfie from the persistent store.

2. Grab a social network from that selfie.

3. Check whether the social network is a fault.

4. Get the name of the social network.

5. Check whether the social network is a fault.

6. Turn the social network back into a fault.

7. Check whether the social network is a fault.

To start, generate NSManagedObject classes for all your Core Data entities. Open SocialNetwork.m or SocialNetwork.swift and implement the methods willTurnIntoFault and didTurnIntoFault. We won’t do anything special in these methods, but rather we will simply log that they’ve been called, so that we can verify that they’re being called appropriately. Listing 9-21 (Objective-C) and Listing 9-22 (Swift) show the implementations of these methods.

Listing 9-21. Handling Fault Status Changes in SocialNetwork.m

- (void)willTurnIntoFault {
NSLog(@"%@ named %@ will turn into fault", [[self class] description], self.name);
}

- (void)didTurnIntoFault {
NSLog(@"%@ did turn into fault", [[self class] description]);
}

Listing 9-22. Handling Fault Status Changes in SocialNetwork.swift

override func willTurnIntoFault() {
println(NSString(format: "%@ named %@ will turn into fault", self, self.name))
}

override func didTurnIntoFault() {
println(NSString(format: "%@ did turn into fault", self))
}

Notice that we log the social network’s name in the willTurnIntoFault method. In the didTurnIntoFault method, however, we don’t log the name. Why? Because we don’t have the name to log. The object is a fault, remember? We don’t have its attributes, including its name, in memory.

Now, create a test called DidFault that conforms to the PerfTest protocol, and then implement its runWithContext: method to perform the seven steps outlined previously. Listing 9-23 shows DidFault.h, Listing 9-24 shows DidFault.m, and Listing 9-25 showsDidFault.swift.

Listing 9-23. DidFault.h

#import "PerfTest.h"

@interface DidFault : NSObject <PerfTest>

@end

Listing 9-24. DidFault.m with the Faulting Test Implemented

#import "DidFault.h"
#import "Selfie.h"
#import "SocialNetwork.h"

@implementation DidFault

-(NSString *)runTestWithContext:(NSManagedObjectContext *)context {
NSString *result = nil;

// 1) Fetch the first selfie
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Selfie"];
fetchRequest.predicate = [NSPredicate predicateWithFormat:@"name = %@", @"Selfie 1"];
NSArray *selfies = [context executeFetchRequest:fetchRequest error:nil];
if ([selfies count] == 1) {
Selfie *selfie = selfies[0];

// 2) Grab a social network from the selfie
SocialNetwork *socialNetwork = [selfie.socialNetworks anyObject];
if (socialNetwork != nil) {
// 3) Check if it's a fault
result = [NSString stringWithFormat:@"Social Network %@ a fault\n",
[socialNetwork isFault] ? @"is" : @"is not"];

// 4) Get the name
result = [result stringByAppendingFormat:@"Social Network is named '%@'\n",
socialNetwork.name];

// 5) Check if it's a fault
result = [result stringByAppendingFormat:@"Social Network %@ a fault\n",
[socialNetwork isFault] ? @"is" : @"is not"];

// 6) Turn it back into a fault
[context refreshObject:socialNetwork mergeChanges:NO];
result = [result stringByAppendingFormat:@"Turning Social Network into a fault\n"];

// 7) Check if it's a fault
result = [result stringByAppendingFormat:@"Social Network %@ a fault\n",
[socialNetwork isFault] ? @"is" : @"is not"];
} else {
result = @"Couldn't find social networks for selfie";
}
} else {
result = @"Failed to fetch first selfie";
}
return result;
}

@end

Listing 9-25. DidFault.swift with the Faulting Test Implemented

import Foundation
import CoreData

class DidFault : PerfTest {
func runTestWithContext(context: NSManagedObjectContext) -> String! {
var result : String?

// 1) Fetch the first selfie
let fetchRequest = NSFetchRequest(entityName: "Selfie")
fetchRequest.predicate = NSPredicate(format: "name = %@", "Selfie 1")
let selfies = context.executeFetchRequest(fetchRequest, error: nil)
if selfies?.count == 1 {
let selfie = selfies?[0] as Selfie

// 2) Grab a social network from the selfie
let socialNetwork = selfie.socialNetworks.anyObject() as SocialNetwork?
if let socialNetwork = socialNetwork {
// 3) Check if it's a fault
result = NSString(format: "Social Network %@ a fault\n", socialNetwork.fault ? "is" : "is not")

// 4) Get the name
result = result?.stringByAppendingFormat("Social Network is named '%@'\n", socialNetwork.name)

// 5) Check if it's a fault
result = result?.stringByAppendingFormat("Social Network %@ a fault\n", socialNetwork.fault ? "is" : "is not")

// 6) Turn it back into a fault
context.refreshObject(socialNetwork, mergeChanges: false)
result = result?.stringByAppendingFormat("Turning Social Network into a fault\n")

// 7) Check if it's a fault
result = result?.stringByAppendingFormat("Social Network %@ a fault\n", socialNetwork.fault ? "is" : "is not")
}
else {
result = "Couldn't find social networks for selfie"
}
}
else {
result = "Failed to fetch first selfie"
}

return result!
}
}

This code follows the seven steps, as promised. Accessing the social network’s name in step 4 is what fires the fault, so that in step 5 the social network is not a fault. Step 6, of course, turns the social network back into a fault.

To add this new test to the picker view, so we can select it and run it, open ViewController.m, add an import for DidFault.h, and add an instance of DidFault to the tests array, as Listing 9-26 shows.

Listing 9-26. Adding an Instance of DidFault in the viewDidLoad Method of ViewController.m

- (void)viewDidLoad {
[super viewDidLoad];

self.startTime.text = @"";
self.stopTime.text = @"";
self.elapsedTime.text = @"";
self.results.text = @"";

self.tests = @[[[FetchAll alloc] init],
[[DidFault alloc] init]];
}

For Swift, add the instance in ViewController.swift, as Listing 9-27 shows.

Listing 9-27. Adding an Instance of DidFault in the viewDidLoad Method of ViewController.swift

override func viewDidLoad() {
super.viewDidLoad()

self.startTime.text = ""
self.stopTime.text = ""
self.elapsedTime.text = ""
self.results.text = ""

self.tests.append(FetchAll())
self.tests.append(DidFault())
}

Build and run the application. You should see DidFault in the picker. Select it and tap Run Selected Test. You should see output similar to the following:

Social Network is a fault
Social Network is named 'SocialNetwork 117'
Social Network is not a fault
Turning Social Network into a fault
Social Network is a fault

You should also see output in the logs from when willTurnIntoFault and didTurnIntoFault were called, like the following:

2014-11-04 07:25:57.149 PerformanceTuning[38171:2069648] SocialNetwork named SocialNetwork 387 will turn into fault
2014-11-04 07:25:57.151 PerformanceTuning[38171:2069648] SocialNetwork did turn into fault

Taking Control: Firing Faults on Purpose

This section on faulting began by explaining that you have more control over memory usage when you run SQL queries yourself than you do by allowing Core Data to manage data fetches. Although true, you can exert some amount of control over Core Data’s fault management by firing faults yourself. By firing faults yourself, you can avoid inefficient scenarios in which Core Data must fire several small faults to fetch your data, incurring several trips to the persistent store.

Core Data provides two means for optimizing the firing of faults.

· Batch faulting

· Prefetching

We are going to test both batch faulting and prefetching. Before we do that, though, we create a test for firing single faults, so that we understand the performance gains of batch faulting and prefetching. Add a class called SingleFault that conforms to the PerfTest protocol. This test should fetch all the selfies and loop through them one at a time while doing the following:

· Access the name attribute so that a fault fires for this selfie only.

· Loop through all the related social networks and access their name attributes, one at a time, so that each access fires a fault.

· Reset each social network so that the next selfie will have to fire faults for each social network.

· Do the same for all the related people.

Listing 9-28 shows the header file, SingleFault.h, and Listing 9-29 shows SingleFault.m. Listing 9-30 shows the Swift implementation, SingleFault.swift.

Listing 9-28. SingleFault.h

#import "PerfTest.h"

@interface SingleFault : NSObject <PerfTest>

@end

Listing 9-29. SingleFault.m

#import "SingleFault.h"
#import "Selfie.h"
#import "SocialNetwork.h"
#import "Person.h"

@implementation SingleFault

- (NSString *)runTestWithContext:(NSManagedObjectContext *)context {
// Fetch all the selfies
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Selfie"];
NSArray *selfies = [context executeFetchRequest:fetchRequest error:nil];

// Loop through all the selfies
for (Selfie *selfie in selfies) {
// Fire a fault just for this selfie
[selfie valueForKey:@"name"];

// Loop through the social networks for this selfie
for (SocialNetwork *socialNetwork in selfie.socialNetworks) {
// Fire a fault just for this social network
[socialNetwork valueForKey:@"name"];

// Put this social network back in a fault
[context refreshObject:socialNetwork mergeChanges:NO];
}

// Loop through the people for this selfie
for (Person *person in selfie.people) {
// Fire a fault for this person
[person valueForKey:@"name"];

// Put this person back in a fault
[context refreshObject:person mergeChanges:NO];
}
}
return @"Test complete";
}

@end

Listing 9-30. SingleFault.swift

import Foundation
import CoreData

class SingleFault : PerfTest {
func runTestWithContext(context: NSManagedObjectContext) -> String! {
// Fetch all the selfies
let fetchRequest = NSFetchRequest(entityName: "Selfie")
let selfies = context.executeFetchRequest(fetchRequest, error: nil)

// Loop through all the selfies
for selfie in selfies as [Selfie] {
// Fire a fault just for this selfie
selfie.valueForKey("name")

// Loop through the social networks for this selfie
for obj in selfie.socialNetworks {
let socialNetwork = obj as SocialNetwork

// Fire a fault just for this social network
socialNetwork.valueForKey("name")

// Put this social network back in a fault
context.refreshObject(socialNetwork, mergeChanges: false)
}

// Loop through the people for this selfie
for obj in selfie.people {
let person = obj as Person

// Fire a fault for this person
person.valueForKey("name")

// Put this person back in a fault
context.refreshObject(person, mergeChanges: false)
}
}

return "Test complete"
}
}

As with the DidFault class, add an instance of SingleFault to the tests array in ViewController.m, as well as an import for SingleFault.h. The tests array should now match Listing 9-31 for Objective-C or Listing 9-32 for Swift.

Listing 9-31. Adding SingleFault to the Tests Array (Objective-C)

self.tests = @[[[FetchAll alloc] init],
[[DidFault alloc] init],
[[SingleFault alloc] init]];

Listing 9-32. Adding SingleFault to the Tests Array (Swift)

self.tests.append(FetchAll())
self.tests.append(DidFault())
self.tests.append(SingleFault())

Before you build and run the application, comment out the willTurnIntoFault and didTurnIntoFault methods of SocialNetwork, so that we don’t fill the logs or skew the timing results. Then, build and run the application and run the SingleFault test. Running this on an iPhone 6 Plus takes just under 30 seconds. Let’s see if we can improve on these results!

Batch Faulting

When you execute a fetch request, you can tell Core Data to return the objects as faults or non-faults by calling NSFetchRequest’s setReturnsObjectsAsFaults: method before executing the fetch request. This method takes a Boolean parameter: pass YES or true to this method to return the fetched objects as faults, or NO or false to return the objects as non-faults. By passing NO or false, you get the objects with all their associated attributes loaded (but, of course, not all the attributes of their relationships).

In this section, we create a test that mirrors the SingleFault test, but instead of firing the faults one by one, it fires them in batches. Create a class called BatchFault that conforms to the PerfTest protocol. By now, you should be able to determine what the header file looks like (it follows the same pattern as DidFault.h and SingleFault.h), so we don’t list it, or any of the other test header files we write in this chapter, here. The implementation file resembles SingleFault.m or SingleFault.swift—it loops through the selfies and, for each selfie, loops through the social networks and people. The difference, however, is that BatchFault uses batch faulting to fault all the objects at once. That is, it faults all the selfies as once, and then for each selfie, it faults all its social networks and people at once. It does this by creating fetch request and calling setReturnsObjectsAsFaults: on them, passing NO or false each time. Listing 9-33 shows the code for BatchFault.m, and Listing 9-34 shows the code for BatchFault.swift.

Listing 9-33. BatchFault.m

#import "BatchFault.h"
#import "Selfie.h"
#import "SocialNetwork.h"
#import "Person.h"

@implementation BatchFault

- (NSString *)runTestWithContext:(NSManagedObjectContext *)context {
// Fetch all the selfies
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Selfie"];

// Return the selfies as non-faults
[fetchRequest setReturnsObjectsAsFaults:NO];
NSArray *selfies = [context executeFetchRequest:fetchRequest error:nil];

// Loop through all the selfies
for (Selfie *selfie in selfies) {
// Doesn't fire a fault, the data are already in memory
[selfie valueForKey:@"name"];

// For this selfie, fire faults for all the social networks
NSFetchRequest *snFetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"SocialNetwork"];
[snFetchRequest setReturnsObjectsAsFaults:NO];
[context executeFetchRequest:snFetchRequest error:nil];

// For this selfie, fire faults for all the social networks
NSFetchRequest *pFetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Person"];
[pFetchRequest setReturnsObjectsAsFaults:NO];
[context executeFetchRequest:pFetchRequest error:nil];

// Loop through the social networks for this selfie
for (SocialNetwork *socialNetwork in selfie.socialNetworks) {
// Doesn't fire a fault, the data are already in memory
[socialNetwork valueForKey:@"name"];

// Put this social network back in a fault
[context refreshObject:socialNetwork mergeChanges:NO];
}

// Loop through the people for this selfie
for (Person *person in selfie.people) {
// Doesn't fire a fault, the data are already in memory
[person valueForKey:@"name"];

// Put this person back in a fault
[context refreshObject:person mergeChanges:NO];
}
}
return @"Test complete";
}

@end

Listing 9-34. BatchFault.swift

import Foundation
import CoreData

class BatchFault : PerfTest {
func runTestWithContext(context: NSManagedObjectContext) -> String! {
// Fetch all the selfies
let fetchRequest = NSFetchRequest(entityName: "Selfie")

// Return the selfies as non-faults
fetchRequest.returnsObjectsAsFaults = true
let selfies = context.executeFetchRequest(fetchRequest, error: nil) as [Selfie]

// Loop through all the selfies
for selfie in selfies {
// Doesn't fire a fault, the data are already in memory
selfie.valueForKey("name")

// For this selfie, fire faults for all the social networks
let snFetchRequest = NSFetchRequest(entityName: "SocialNetwork")
snFetchRequest.returnsObjectsAsFaults = false
context.executeFetchRequest(snFetchRequest, error: nil)

// For this selfie, fire faults for all the people
let pFetchRequest = NSFetchRequest(entityName: "Person")
pFetchRequest.returnsObjectsAsFaults = false
context.executeFetchRequest(pFetchRequest, error: nil)

// Loop through the social networks for this selfie
for obj in selfie.socialNetworks {
let socialNetwork = obj as SocialNetwork

// Doesn't fire a fault, the data are already in memory
socialNetwork.valueForKey("name")

// Put this social network back in a fault
context.refreshObject(socialNetwork, mergeChanges: false)
}

// Loop through the people for this selfie
for obj in selfie.people {
let person = obj as Person

// Doesn't fire a fault, the data are already in memory
person.valueForKey("name")

// Put this person back in a fault
context.refreshObject(person, mergeChanges: false)
}
}

return "Test complete"
}
}

Add an instance of BatchFault to the tests array in ViewController.m (with an import for BatchFault.h) or ViewController.swift, as we did for the other tests, and build and run the program. Run the BatchFault test and check the elapsed time. On our iPhone 6 Plus it takes . . . a little over 30 seconds. That’s slower than the SingleFault test. That’s disappointing, but it shows the importance of practice over theory. Theoretically, firing the faults in batches should be much faster than firing them singly. In practice, however, we haven’t gained any performance—in fact, we’ve lost some—and have made the code a little less readable. Let’s see if prefetching works any better.

Prefetching

Similar to batch faulting, prefetching minimizes the number of times that Core Data has to fire faults and go fetch data. With prefetching, though, you tell Core Data when you perform a fetch to also fetch the related objects you specify. For example, using this chapter’s data model, when you fetch the selfies, you can tell Core Data to prefetch the related social networks, people, or both.

To prefetch related objects, call NSFetchRequest’s setRelationshipKeyPathsForPrefetching: method, passing an array that contains the names of the relationships that you want Core Data to prefetch. To prefetch the related social networks and people when you fetch the selfies, for example, use the following Objective-C code:

NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Selfie"];
[fetchRequest setRelationshipKeyPathsForPrefetching:@[@"socialNetworks", @"people"]];
NSArray *selfies = [context executeFetchRequest:fetchRequest error:nil];

In Swift, it looks like the following:

let fetchRequest = NSFetchRequest(entityName: "Selfie")
fetchRequest.relationshipKeyPathsForPrefetching = ["socialNetwork", "people"]
let selfies = context.executeFetchRequest(fetchRequest, error: nil) as [Selfie]

The lines in bold instruct Core Data to prefetch all the related social networks and people when it fetches the selfies. Note that you’ll get no error if the strings you pass don’t match any existing relationship names, so check your spelling carefully. You may think you’re prefetching relationships, but an errant misspelling may be denying your performance gains.

To test prefetching, create a class called Prefetch that conforms to the PerfTest protocol. In the implementation of runTestWithContext:, use essentially the same code as SingleFault, but this time add a call to setRelationshipKeyPathsForPrefetching:, passing an array containing the names of the relationships. Listing 9-35 shows the code for Prefetch.m, and Listing 9-36 shows the code for Prefetch.swift.

Listing 9-35. Prefetch.m

#import "Prefetch.h"
#import "Selfie.h"
#import "SocialNetwork.h"
#import "Person.h"

@implementation Prefetch

- (NSString *)runTestWithContext:(NSManagedObjectContext *)context {
// Set up the fetch request for all the selfies
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Selfie"];

// Prefetch the social networks and people
[fetchRequest setRelationshipKeyPathsForPrefetching:@[@"socialNetworks", @"people"]];

// Perform the fetch
NSArray *selfies = [context executeFetchRequest:fetchRequest error:nil];

// Loop through the selfies
for (Selfie *selfie in selfies) {
// Fire a fault just for this selfie
[selfie valueForKey:@"name"];

// Loop through the social networks for this selfie
for (SocialNetwork *socialNetwork in selfie.socialNetworks) {
// Fire a fault just for this social network
[socialNetwork valueForKey:@"name"];

// Put this social network back in a fault
[context refreshObject:socialNetwork mergeChanges:NO];
}

// Loop through the people for this selfie
for (Person *person in selfie.people) {
// Fire a fault for this person
[person valueForKey:@"name"];

// Put this person back in a fault
[context refreshObject:person mergeChanges:NO];
}
}
return @"Test complete";
}

@end

Listing 9-36. Prefetch.swift

import Foundation
import CoreData

class Prefetch : PerfTest {
func runTestWithContext(context: NSManagedObjectContext) -> String! {
// Set up the fetch request for all the selfies
let fetchRequest = NSFetchRequest(entityName: "Selfie")

// Prefetch the social networks and people
fetchRequest.relationshipKeyPathsForPrefetching = ["socialNetwork", "people"]

// Perform the fetch
let selfies = context.executeFetchRequest(fetchRequest, error: nil) as [Selfie]

// Loop through the selfies
for selfie in selfies {
// For a fault just for the selfie
selfie.valueForKey("name")

// Loop through the social networks for this selfie
for obj in selfie.socialNetworks {
let socialNetwork = obj as SocialNetwork

// Fire a fault for this social network
socialNetwork.valueForKey("name")

// Put this social network back in a fault
context.refreshObject(socialNetwork, mergeChanges: false)
}

// Loop through the people for this selfie
for obj in selfie.people {
let person = obj as Person

// Fire a fault for this person
person.valueForKey("name")

// Put this person back in a fault
context.refreshObject(person, mergeChanges: false)
}
}

return "Test complete"
}
}

Add a Prefetch instance to the tests array in ViewController.m or ViewController.swift, as well as an import for Prefetch.h (if you’re doing Objective-C), and build and run the application. Run the Prefetch test. On our iPhone 6 Plus, it takes just under 3 seconds—about a 90% improvement! As with batch faulting you should test prefetching with your own data model instead of coming to absolute conclusions, but prefetching is obviously a candidate for improving fetch speed for large data models with relationships.

Caching

Regardless of target language or platform, most data persistence frameworks and libraries have an internal caching mechanism. Properly implemented caches provide opportunities for significant performance gains, especially for applications that need to retrieve the same data repeatedly. Core Data is no exception. The NSManagedObjectContext class serves as a built-in cache for the Core Data framework. When you retrieve an object from the backing persistent store, the context keeps a reference to it to track its changes. If you retrieve the object again, the context can give the caller the same object reference as it did in the first invocation.

The obvious trade-off that results from the use of caching is that, while improving performance, caching uses more memory. If no cache management scheme were in place to limit the memory usage of the cache, the cache could fill up with objects until the whole system collapses from lack of memory. To manage memory, the Core Data context has weak references to the managed objects it pulls out of the persistent store. This means that if the retain count of a managed object reaches zero because no other object has a reference to it, the managed object will be discarded. The exception to this rule is if the object has been modified in any way. In this case, the context keeps a strong reference (i.e., sends a retain signal to the managed object) and keeps it until the context is either committed or rolled back, at which point it becomes a weak reference again.

Note The default retain behavior can be changed by setting the returnsObjectsAsFaults property of NSManagedObjectContext to YES or true. Setting it to YES or true will cause the context to retain all registered objects. The default behavior retains registered objects only when they are inserted, updated, deleted, or locked.

In this section, you will examine the difference between fetching objects from the persistent store or from the cache. You will build a test that does the following:

1. Retrieves all selfies.

2. Retrieves all social networks for each selfie.

3. Displays the time it took to perform both retrievals.

4. Retrieves all selfies (this time the objects will be cached).

5. Retrieves all social networks for each selfie.

6. Displays the time it took to perform both retrievals.

Create a test class, conforming to the PerfTest protocol, called Cache. Listing 9-37 shows the code for Cache.m, and Listing 9-38 shows the code for Cache.swift.

Listing 9-37. Cache.m

#import "Cache.h"
#import "Selfie.h"
#import "SocialNetwork.h"
#import "Person.h"

@implementation Cache

- (NSString *)runTestWithContext:(NSManagedObjectContext *)context {
NSMutableString *result = [NSMutableString string];

// Load the data while it's not cached
NSDate *start1 = [NSDate date];
[self loadDataWithContext:context];
NSDate *end1 = [NSDate date];

// Record the results
[result appendFormat:@"Without cache: %.3f s\n", [end1 timeIntervalSinceDate:start1]];

// Load the data while it's cached
NSDate *start2 = [NSDate date];
[self loadDataWithContext:context];
NSDate *end2 = [NSDate date];

// Record the results
[result appendFormat:@"With cache: %.3f s\n", [end2 timeIntervalSinceDate:start2]];

return result;
}

- (void)loadDataWithContext:(NSManagedObjectContext *)context {
// Load the selfies
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Selfie"];
NSArray *selfies = [context executeFetchRequest:fetchRequest error:nil];

// Loop through the selfies
for (Selfie *selfie in selfies) {
// Load the selfie's data
[selfie valueForKey:@"name"];

// Loop through the social networks
for (SocialNetwork *socialNetwork in selfie.socialNetworks) {
// Load the social network's data
[socialNetwork valueForKey:@"name"];
}
}
}

@end

Listing 9-38. Cache.swift

import Foundation
import CoreData

class Cache : PerfTest {
func runTestWithContext(context: NSManagedObjectContext) -> String! {
let result = NSMutableString()

// Load the data while it's not cached
let start1 = NSDate()
self.loadDataWithContext(context)
let end1 = NSDate()

// Record the results
result.appendFormat("Without cache: %.3f s\n", end1.timeIntervalSinceDate(start1))

// Load the data while it's cached
let start2 = NSDate()
self.loadDataWithContext(context)
let end2 = NSDate()

// Record the results
result.appendFormat("With cache: %.3f s\n", end2.timeIntervalSinceDate(start2))

return result
}

func loadDataWithContext(context: NSManagedObjectContext) {
// Load the selfies
let fetchRequest = NSFetchRequest(entityName: "Selfie")
let selfies = context.executeFetchRequest(fetchRequest, error: nil) as [Selfie]

// Loop through the selfies
for selfie in selfies {
// Load the selfie's data
selfie.valueForKey("name")

// Loop through the social networks
for obj in selfie.socialNetworks {
let socialNetwork = obj as SocialNetwork

// Load the social network's data
socialNetwork.valueForKey("name")
}
}
}
}

Let’s examine the runWithContext: method. It does the same thing twice: fetches all the selfies and all the social networks. The first time, the cache is explicitly cleared, as it is before each test, by calling [context reset] or context.reset() in the runTest: handler inViewController. The second time, the objects will be in the cache, and therefore the data will come back much faster. The loadDataWithContext: method does the actual fetches. You explicitly get the name property of the selfies and social networks in order to force Core Data to load the attributes for the objects, firing a fault if necessary.

Add an instance of Cache to the tests array in ViewController.m or ViewController.swift, and also an import for Cache.h if using Objective-C. Build the application and run the Cache test. On our iPhone 5s, the test takes about 0.6 seconds without a cache, and about 0.09 seconds with the cache, which is about an 85% improvement. The lesson, then, is to be judicious about expiring your cache by calling reset on your managed object context, as its caching can improve performance significantly. The next section discusses cache expiration strategies.

Expiring the Cache

Any time an application uses a cache, the question of cache expiration arises: when should objects in the cache expire and be reloaded from the persistent store? The difficulty in determining the expiration interval comes from juggling the performance gain obtained by caching objects for long intervals versus the extra memory consumption this entails and the potential staleness of the cached data. This section examines the trade-offs of the two possibilities for expiring the cache and freeing some memory.

Memory Consumption

As more and more objects are put into the cache, memory usage increases. Even if a managed object is entirely faulted, you can determine the minimum amount of memory an NSManagedObject instance uses by calling the following:

NSLog(@"%zu", class_getInstanceSize(NSManagedObject.class)); // Objective-C
println(String(format: "%zu", class_getInstanceSize(NSManagedObject.self))) // Swift

Note that, for the class_getInstanceSize() function, you must import <objc/objc-runtime.h> in Objective-C. If you’re running on a 32-bit system, you’ll see that the allocated size of an unpopulated managed object is 48 bytes. On a 64-bit system, the size is 96 bytes. This is because it holds references (i.e., 4-byte or 8-byte pointers, depending on the bitness of the operating system) to other objects such as the entity description, the context, and the object ID. Even without any actual data, a managed object occupies a minimum of 48 bytes (32-bit OS) or 96 bytes (64-bit OS). This is a best-case scenario because this approximation does not include the memory occupied by the unique object ID, which is populated. This means that if you have 100,000 managed objects in the cache, even faulted, you are using at least 5MB (32-bit OS) or 10MB (64-bit OS) of memory for things other than your data. If you start fetching data without faulting, you can run into memory issues quickly.

The trick to this balancing act is to remove data from the cache when it’s no longer needed or if you can afford to pay the price of retrieving the objects from the persistent store when you need them again.

Brute-Force Cache Expiration

If you don’t care about losing all the managed objects, you can reset the context entirely. This is rarely the option you want to choose, but it is extremely efficient. NSManagedObjectContext, as we’ve seen, has a reset method that will wipe the cache out in one swoop. Once you call[context reset] or context.reset(), your memory footprint will be dramatically smaller, but you will have to pay the price of going to the persistent store if you want to retrieve any objects again. Please also understand that, as with any other kind of mass destruction mechanism, resetting the cache in the middle of a running application has serious side effects and collateral damage. For example, any managed objects that you were using prior to the reset are now invalid. If you try to do anything with them, your efforts will be met with runtime errors.

Expiring the Cache Through Faulting

Faulting is a more subtle option, as the section on faulting explains. You can fault any managed object by calling [context refreshObject:managedObject mergeChanges:NO] (or the Swift equivalent). After this method call, the object is faulted, and therefore the memory it occupies in the cache is minimized, although not zero. A non-negligible advantage of this strategy, however, is that when the managed object is turned into a fault, any managed object it has a reference to (through relationships) is released. If those related managed objects have no other references to them, then they will be removed from the cache, further reducing the memory footprint. Faulting managed objects in this manner helps prune the entire object graph.

Uniquing

Both business and technology like to turn nouns into verbs, and the pseudoword “uniquing” testifies to this weakness. It attempts to define the action of making something unique or ensuring uniqueness. Usage suggests not only that Apple didn’t invent the term but also that it predates Core Data. Apple embraces the term in its Core Data documentation, however, raising fears that one day Apple will call the action of listening to music iPodding.

The technology industry uses the term “uniquing” in conjunction with memory objects versus their representation in a data store. In Core Java Data Objects by Sameer Tyagi and Michael Vorburger (Prentice Hall, 2003), it says that uniquing “ensures that no matter how many times a persistent object is found, it has only one in-memory representation. All references to the same persistent object within the scope of the same PersistenceManager instance reference the same in-memory object.”

Martin Fowler, in Patterns of Enterprise Application Architecture (Addison-Wesley Professional, 2005), gives it a less colorful, more descriptive, and more English-compliant name: identity map. He explains that an identity map “ensures that each object gets loaded only once by keeping every loaded object in a map.” Whatever you call it or however you describe it, uniquing means that Core Data conserves memory use by ensuring the uniqueness of each object in memory and that no two memory instances of an object point to the same instance in the persistent store.

Consider, for example, the Core Data model in the application in this chapter. Each Selfie instance has a relationship with 500 Person instances. When you run the application and reference the Person instances through any Selfie instances, you always get the same Personinstances, as Figure 9-6 depicts. If Core Data didn’t use uniquing, you could find yourself in the scenario shown in Figure 9-7, where each Person instance is represented in memory several times, once for each Selfie instance, and each Selfie instance is represented in memory several times, once for each Person instance.

image

Figure 9-6. Uniquing selfies and people

image

Figure 9-7. Nonuniquing selfies and people

Uniquing not only conserves memory but also eliminates data inconsistency issues. Think what could happen, for example, if Core Data didn’t employ uniquing and the PerformanceTuning application had 500 memory instances of each selfie (one for each person). Suppose that Person 1changed the rating of a selfie to 5 and Person 2 changed the rating of the same selfie to 7. What rating should the selfie really have? When the application stores the selfie in the persistent store, what rating should it save? You can imagine the data inconsistency bugs you’d have to track down if Core Data maintained more than one instance of each data object in memory.

Note that uniquing occurs within a single managed object context only, not across managed object contexts. The good news, however, is that Core Data’s default behavior, which you can’t change, is to unique. Uniquing comes free with Core Data.

To test uniquing, create a test called Uniquing (again, make it conform to PerfTest) that fetches all the selfies from the persistent store and then compares each of their related people to each other selfie’s related people. For the test to pass, the code must verify that only 500 Personinstances live in memory and that each selfie points to the same 500 Person instances. To make these comparisons easier, the code sorts the people into a determined order so you can predictably compare instances. The first time through the loop for the first selfie, the code stores the people in a reference array so that all subsequent loops have something to compare the people against. In each subsequent loop, the code pulls the people for the selfie and compares them to the reference array. If just one person doesn’t match, the test fails. Listing 9-39 shows the code forUniquing.m, and Listing 9-40 shows the code for Uniquing.swift.

Listing 9-39. Uniquing.m

#import "Uniquing.h"
#import "Selfie.h"
#import "Person.h"

@implementation Uniquing

- (NSString *)runTestWithContext:(NSManagedObjectContext *)context {
// Array to hold the people for comparison purposes
NSArray *referencePeople = nil;

// Sorting for the people
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"name"
ascending:YES];

// Fetch all the selfies
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Selfie"];
NSArray *selfies = [context executeFetchRequest:fetchRequest error:nil];

// Loop through the selfies
for (Selfie *selfie in selfies) {
// Get the sorted people
NSArray *people = [selfie.people sortedArrayUsingDescriptors:@[sortDescriptor]];

// Store the first selfie's people for comparison purposes
if (referencePeople == nil) {
referencePeople = people;
} else {
// Do the comparison
for (int i = 0, n = (int)[people count]; i < n; i++) {
if (people[i] != referencePeople[i]) {
return [NSString stringWithFormat:@"Uniquing test failed; %@ != %@",
people[i], referencePeople[i]];
}
}
}
}
return @"Test complete";
}

@end

Listing 9-40. Uniquing.swift

import Foundation
import CoreData

class Uniquing : PerfTest {
func runTestWithContext(context: NSManagedObjectContext) -> String! {
// Array to hold the people for comparison purposes
var referencePeople : [Person]?

// Sorting for the people
let sortDescriptor = NSSortDescriptor(key: "name", ascending: true)

// Fetch all the selfies
let fetchRequest = NSFetchRequest(entityName: "Selfie")
let selfies = context.executeFetchRequest(fetchRequest, error: nil) as [Selfie]

// Loop through the selfies
for selfie in selfies {
// Get the sorted people
let people = selfie.people.sortedArrayUsingDescriptors([sortDescriptor]) as [Person]

// Store the first selfie's people for comparison purposes
if let referencePeople = referencePeople {
// Do the comparison
for var i=0, n = people.count; i<n; i++ {
if people[i] != referencePeople[i] {
return NSString(format: "Uniquing test failed; %@ != %@", people[i], referencePeople[i])
}
}
}
else {
referencePeople = people
}
}

return "Test complete"
}
}

Add an instance of Uniquing to the tests array in ViewController.m or ViewController.swift, add an import for Uniquing.h if necessary, build and run the application, and run the new test. You’ll eventually see the “Test complete” message, indicating that the test passed and uniquing worked.

Improve Performance with Better Predicates

Despite the year in the movie title 2001: A Space Odyssey, we are still far away from our machines having the intelligence to respond with things like “I’m sorry, Dave. I’m afraid I can’t do that.” As of the time of writing this book, programmers still have to do a lot of hand-holding to walk their machines through the process of doing what they are asked to do. This means that how you write your code determines how efficient it will be. There are often multiple ways to retrieve the same data, but the solution you use can significantly alter the performance of your application.

Using Faster Comparators

Generally speaking, string comparators perform more slowly than primitive comparators. When predicates are compounded using an OR operator, it is always more efficient to put the primitive comparators first because if they resolve to TRUE, then the rest of the comparators don’t need to be evaluated. This is because “TRUE OR anything” is always true. A similar strategy can be used with AND-compounded predicates. In this case, if the first predicate fails, then the second will not be evaluated, because “FALSE AND anything” is always false.

To validate this, add a new test to your performance test application called Predicate and make it conform to the PerfTest protocol. This test consists of two fetch requests, which retrieve the same objects using an OR-compounded predicate. In the first test, the string comparator LIKEis used before the primitive comparator. In the second test, the comparators are permuted. Run each test 1,000 times in order to get significant timings; each time through the loop, the cache is reset to keep a clean context. Listing 9-41 shows Predicate.m and Listing 9-42 showsPredicate.swift.

Listing 9-41. Predicate.m

#import "Predicate.h"

@implementation Predicate

- (NSString *)runTestWithContext:(NSManagedObjectContext *)context {
NSMutableString *result = [NSMutableString string];

// Set up the first fetch request
NSFetchRequest *fetchRequest1 = [NSFetchRequest fetchRequestWithEntityName:@"Selfie"];
fetchRequest1.predicate = [NSPredicate predicateWithFormat:@"(name LIKE %@) OR (rating < %d)", @"*e*ie*", 5];

// Run the first fetch request and measure
NSDate *start1 = [NSDate date];
for (int i = 0; i < 1000; i++) {
[context reset];
[context executeFetchRequest:fetchRequest1 error:nil];
}
NSDate *end1 = [NSDate date];
[result appendFormat:@"Slow predicate: %.3f s\n", [end1 timeIntervalSinceDate:start1]];

// Set up the second fetch request
NSFetchRequest *fetchRequest2 = [NSFetchRequest fetchRequestWithEntityName:@"Selfie"];
fetchRequest1.predicate = [NSPredicate predicateWithFormat:@"(rating < %d) OR (name LIKE %@)", 5, @"*e*ie*"];

// Run the first fetch request and measure
NSDate *start2 = [NSDate date];
for (int i = 0; i < 1000; i++) {
[context reset];
[context executeFetchRequest:fetchRequest2 error:nil];
}
NSDate *end2 = [NSDate date];
[result appendFormat:@"Fast predicate: %.3f s\n", [end2 timeIntervalSinceDate:start2]];

return result;
}

@end

Listing 9-42. Predicate.swift

import Foundation
import CoreData

class Predicate : PerfTest {
func runTestWithContext(context: NSManagedObjectContext) -> String! {
let result = NSMutableString()

// Set up the first fetch request
let fetchRequest1 = NSFetchRequest(entityName: "Selfie")
fetchRequest1.predicate = NSPredicate(format: "(name LIKE %@) OR (rating < %d)", "*e*ie*", 5)

// Run the first fetch request and measure
let start1 = NSDate()
for var i=0; i<1000; i++ {
context.reset()
context.executeFetchRequest(fetchRequest1, error: nil)
}
let end1 = NSDate()
result.appendFormat("Slow predicate: %.3f s\n", end1.timeIntervalSinceDate(start1))

// Set up the second fetch request
let fetchRequest2 = NSFetchRequest(entityName: "Selfie")
fetchRequest2.predicate = NSPredicate(format: "(rating < %d) OR (name LIKE %@)", 5, "*e*ie*")

// Run the first fetch request and measure
let start2 = NSDate()
for var i=0; i<1000; i++ {
context.reset()
context.executeFetchRequest(fetchRequest2, error: nil)
}
let end2 = NSDate()
result.appendFormat("Fast predicate: %.3f s\n", end2.timeIntervalSinceDate(start2))

return result
}
}

After importing Predicate.h into ViewController.m and adding a Predicate instance to the tests array, build and run the application, run the Predicate test, and note the times for the slow predicate versus the fast predicate. On our iPhone 6 Plus, the slow predicate takes about 6.3 seconds, while the fast predicate takes about a third of the time: about 2.3 seconds.

Using Subqueries

You saw in Chapter 3 how to use subqueries to help simplify the code. In this section, you add a test to show the difference between using subqueries and retrieving related data manually. Consider an example in which you want to find all people from a selfie who match certain criteria. To do this without using a subquery, you have to first fetch all the selfies that match the criteria. You then have to iterate through each selfie and extract the people. You then go through the people and add them to your result set, making sure you don’t duplicate people if they appear in multiple selfies. Listing 9-43 shows the manual subquery code in Objective-C, and Listing 9-44 shows the Swift version.

Listing 9-43. Doing a Subquery the Hard Way (Objective-C)

NSMutableDictionary *people = [NSMutableDictionary dictionary];

NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Selfie"];
fetchRequest.predicate = [NSPredicate predicateWithFormat:@"(rating < %d) OR (name LIKE %@)", 5, @"*e*ie*"];
NSArray *selfies = [context executeFetchRequest:fetchRequest error:nil];

for (Selfie *selfie in selfies) {
for (Person *person in selfie.people) {
[people setObject:person forKey:[[[person objectID] URIRepresentation] description]];
}
}

Listing 9-44. Doing a Subquery the Hard Way (Swift)

var people = [String: Person]()

let fetchRequest = NSFetchRequest(entityName: "Selfie")
fetchRequest.predicate = NSPredicate(format: "(rating < %d) OR (name LIKE %@)", 5, "*e*ie*")
let selfies = context.executeFetchRequest(fetchRequest, error: nil) as [Selfie]

for selfie in selfies {
for obj in selfie.people {
let person = obj as Person
let key : String = person.objectID.URIRepresentation().description
people[key] = person
}
}

In this implementation, the people dictionary contains all the matching people. You keyed them by objectID to eliminate duplicates. The alternative to this approach is to let Core Data do the matching for you by using subqueries. Listing 9-45 (Objective-C) or Listing 9-46 (Swift) shows the same result set using a subquery.

Listing 9-45. Doing a Subquery the Right Way (Objective-C)

NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Person"];
fetchRequest.predicate = [NSPredicate predicateWithFormat:@"(SUBQUERY(selfies, $x, ($x.rating < %d) OR ($x.name LIKE %@)).@count > 0)", 5, @"*e*ie*"];
NSArray *people = [context executeFetchRequest:fetchRequest error:nil];

Listing 9-46. Doing a Subquery the Right Way (Swift)

let fetchRequest = NSFetchRequest(entityName: "Person")
fetchRequest.predicate = NSPredicate(format: "(SUBQUERY(selfies, $x, ($x.rating < %d) OR ($x.name LIKE %@)).@count > 0)", 5, "*e*ie*")
let people = context.executeFetchRequest(fetchRequest2, error: nil) as [Selfie]

One of the major differences here is that you let the persistent store do all the work of retrieving the matching people, which means that most of the results don’t land in the context layer of Core Data for you to post-process. With the subquery, you don’t actually retrieve the selfies, as you did in the manual approach, and the fetched request is set up to fetch people directly. Also, the manual option has to fire faults to retrieve the people after retrieving the selfies.

To demonstrate the improved efficiency of the subquery approach, create a new PerfTest-conforming class called Subquery. Listing 9-47 shows Subquery.m, and Listing 9-48 shows Subquery.swift.

Listing 9-47. Subquery.m

#import "Subquery.h"
#import "Selfie.h"
#import "Person.h"

@implementation Subquery

- (NSString *)runTestWithContext:(NSManagedObjectContext *)context {
NSMutableString *result = [NSMutableString string];

// Set up the first fetch request, with no subquery
NSFetchRequest *fetchRequest1 = [NSFetchRequest fetchRequestWithEntityName:@"Selfie"];
fetchRequest1.predicate = [NSPredicate predicateWithFormat:@"(rating < %d) OR (name LIKE %@)", 5, @"*e*ie*"];

// Mark the time and get the results
NSDate *start1 = [NSDate date];
NSArray *selfies = [context executeFetchRequest:fetchRequest1 error:nil];
NSMutableDictionary *people1 = [NSMutableDictionary dictionary];
for (Selfie *selfie in selfies) {
for (Person *person in selfie.people) {
[people1 setObject:person forKey:[[[person objectID] URIRepresentation] description]];
}
}
NSDate *end1 = [NSDate date];

// Record the results of the manual request
[result appendFormat:@"No subquery: %.3f s\n", [end1 timeIntervalSinceDate:start1]];
[result appendFormat:@"People retrieved: %ld\n", [people1 count]];

// Reset the context so we get clean results
[context reset];

// Set up the second fetch request, with subquery
NSFetchRequest *fetchRequest2 = [NSFetchRequest fetchRequestWithEntityName:@"Person"];
fetchRequest2.predicate = [NSPredicate predicateWithFormat:@"(SUBQUERY(selfies, $x, ($x.rating < %d) OR ($x.name LIKE %@)).@count > 0)", 5, @"*e*ie*"];

// Mark the time and get the results
NSDate *start2 = [NSDate date];
NSArray *people2 = [context executeFetchRequest:fetchRequest2 error:nil];
NSDate *end2 = [NSDate date];

// Record the results of the subquery request
[result appendFormat:@"Subquery: %.3f s\n", [end2 timeIntervalSinceDate:start2]];
[result appendFormat:@"People retrieved: %ld\n", [people2 count]];

return result;
}

@end

Listing 9-48. Subquery.swift

#import "Subquery.h"
#import "Selfie.h"
#import "Person.h"

@implementation Subquery

- (NSString *)runTestWithContext:(NSManagedObjectContext *)context {
NSMutableString *result = [NSMutableString string];

// Set up the first fetch request, with no subquery
NSFetchRequest *fetchRequest1 = [NSFetchRequest fetchRequestWithEntityName:@"Selfie"];
fetchRequest1.predicate = [NSPredicate predicateWithFormat:@"(rating < %d) OR (name LIKE %@)", 5, @"*e*ie*"];

// Mark the time and get the results
NSDate *start1 = [NSDate date];
NSArray *selfies = [context executeFetchRequest:fetchRequest1 error:nil];
NSMutableDictionary *people1 = [NSMutableDictionary dictionary];
for (Selfie *selfie in selfies) {
for (Person *person in selfie.people) {
[people1 setObject:person forKey:[[[person objectID] URIRepresentation] description]];
}
}
NSDate *end1 = [NSDate date];

// Record the results of the manual request
[result appendFormat:@"No subquery: %.3f s\n", [end1 timeIntervalSinceDate:start1]];
[result appendFormat:@"People retrieved: %ld\n", [people1 count]];

// Reset the context so we get clean results
[context reset];

// Set up the second fetch request, with subquery
NSFetchRequest *fetchRequest2 = [NSFetchRequest fetchRequestWithEntityName:@"Person"];
fetchRequest2.predicate = [NSPredicate predicateWithFormat:@"(SUBQUERY(selfies, $x, ($x.rating < %d) OR ($x.name LIKE %@)).@count > 0)", 5, @"*e*ie*"];

// Mark the time and get the results
NSDate *start2 = [NSDate date];
NSArray *people2 = [context executeFetchRequest:fetchRequest2 error:nil];
NSDate *end2 = [NSDate date];

// Record the results of the subquery request
[result appendFormat:@"Subquery: %.3f s\n", [end2 timeIntervalSinceDate:start2]];
[result appendFormat:@"People retrieved: %ld\n", [people2 count]];

return result;
}

@end

Add your import (Subquery.h) and your Subquery instance to ViewController, and then run the test. On our iPhone 6 Plus, the no-subquery version takes just over 30 seconds, while the subquery version takes about 1.5 seconds—over 20 times faster!

How you write your predicates can have a profound effect on the performance of your application. You should always be mindful of what you are asking Core Data to do and how you can accomplish the same results with more efficient predicates.

Batch Updates

In the pre-iOS Core Data world, if you wanted to update a raft of Core Data objects, you had to fetch all the objects you wanted to update, iterate through them, set the new value or values, and then save the managed object context. Depending on the save of the data set you wish to update, this can be a slow and memory-intensive operation, as all the objects had to be fetched into memory and then written to in an iterative loop. For large updates—updates too large to fit in memory, perhaps—developers had to create custom solutions to break the updates into smaller batches.

iOS 8 (and OS X Yosemite) introduced batch updates for Core Data to address this gap in the form of a class called NSBatchUpdateRequest. Surprisingly, the Apple documentation (as of Xcode 6.1 and iOS 8.1) omit any documentation for this class except to show that it inherits fromNSPersistentStoreRequest. The header file, NSBatchUpdateRequest.h, gives us a little info, though. Here are two important points we learn from the header file’s comments.

· NSBatchUpdateRequest talks directly to the underlying persistent store without loading data into memory. Not all persistent stores support this approach, but since you’re most likely using a SQLite persistent store, you should be OK.

· Because it talks directly to the underlying persistent store, it doesn’t go through the validation that the managed object model and managed object context provide. You are responsible for enforcing the validation rules yourself.

To execute a batch update request, you create an NSBatchUpdateRequest instance, specifying the entity from your model that the request will update. Optionally, you set a predicate on the request to filter the objects to update. You set the properties to update and the values to update them to. You also set the type of result you want returned. Finally, you run the update.

You specify the properties to update in a dictionary, with the name of the property to update as the key and the desired value as the value. This necessarily means that you are specifying a single value for a given property that all matching objects will be updated to—you can’t specify different values for each object. This makes sense; Core Data is going to generate and execute a SQL statement that looks something like the following:

UPDATE ZMYENTITY SET ZNAME = ?

You can, of course, set multiple properties to update.

For the type of result you want returned from running the batch update, you can specify one of three values.

· NSStatusOnlyResultType—return nothing. This is the default.

· NSUpdateObjectIDsResultType—return the object IDs of the updated objects.

· NSUpdatedObjectsCountResultType—return the number of objects that were updated.

The return type you’ll want to specify depends on your application’s needs, of course. The NSUpdateObjectIDsResultType can be useful, though, so that you can fault the updated objects in your managed object context. Remember that the batch update bypasses the managed object context, so if you have the objects in your managed object context, they’re sitting there with the old value or values. You can fault them and then refetch them to get the updated objects.

Whatever type you specify, executing the batch update sets the result member of the NSBatchUpdateResult to the appropriate value.

To test batch updates, create a PerfTest-conforming class called BatchUpdate. In the test this method implements, we update the rating for all the selfies twice. The first time we perform the update, we do it the pre-iOS 8 way: one at a time. We fetch all the selfies, then iterate through them to set their rating values to 5. Finally, we save the managed object context. The second time we perform the update, we use a batch update to set the rating values to 7. Listing 9-49 shows the Objective-C version, and Listing 9-50 shows the Swift version.

Listing 9-49. BatchUpdate.m

#import "BatchUpdate.h"

@implementation BatchUpdate

- (NSString *)runTestWithContext:(NSManagedObjectContext *)context {
NSMutableString *result = [NSMutableString string];

// Update all the selfies one at a time
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Selfie"];
NSDate *start1 = [NSDate date];
NSArray *selfies = [context executeFetchRequest:fetchRequest error:nil];
for (NSManagedObject *selfie in selfies) {
[selfie setValue:@5 forKey:@"rating"];
}
[context save:nil];
NSDate *end1 = [NSDate date];
[result appendFormat:@"One-at-a-time update: %.3f s\n", [end1 timeIntervalSinceDate:start1]];

// Update the selfies as a batch

// Create the batch update request for the Selfie entity
NSBatchUpdateRequest *batchUpdateRequest = [NSBatchUpdateRequest batchUpdateRequestWithEntityName:@"Selfie"];

// Set the desired result type to be the count of updated objects
batchUpdateRequest.resultType = NSUpdatedObjectsCountResultType;

// Update the rating property to 7
batchUpdateRequest.propertiesToUpdate = @{ @"rating" : @7 };

// Mark the time and run the update
NSDate *start2 = [NSDate date];
NSBatchUpdateResult *batchUpdateResult = (NSBatchUpdateResult *)[context executeRequest:batchUpdateRequest error:nil];
NSDate *end2 = [NSDate date];

// Record the results
[result appendFormat:@"Batch update (%@ rows): %.3f s\n", batchUpdateResult.result, [end2 timeIntervalSinceDate:start2]];

return result;
}

@end

Listing 9-50. BatchUpdate.swift

import Foundation
import CoreData

class BatchUpdate : PerfTest {
func runTestWithContext(context: NSManagedObjectContext) -> String! {
let result = NSMutableString()

// Update all the selfies one at a time
let fetchRequest = NSFetchRequest(entityName: "Selfie")
let start1 = NSDate()
let selfies = context.executeFetchRequest(fetchRequest, error: nil) as [Selfie]
for selfie in selfies {
selfie.setValue(5, forKey:"rating")
}
context.save(nil)
let end1 = NSDate()
result.appendFormat("One-at-a-time update: %.3f s\n", end1.timeIntervalSinceDate(start1))

// Update the selfies as a batch

// Create the batch update request for the Selfie entity
let batchUpdateRequest = NSBatchUpdateRequest(entityName: "Selfie")

// Set the desired result type to be the count of updated objects
batchUpdateRequest.resultType = .UpdatedObjectsCountResultType;

// Update the rating property to 7
batchUpdateRequest.propertiesToUpdate = ["rating" : 7];

// Mark the time and run the update
let start2 = NSDate()
let batchUpdateResult = context.executeRequest(batchUpdateRequest, error: nil) as NSBatchUpdateResult
let end2 = NSDate()

// Record the results
result.appendFormat("Batch update (\(batchUpdateResult.result!) rows): %.3f s\n", end2.timeIntervalSinceDate(start2))

return result
}
}

Add a BatchUpdate to your tests array, build and run the application, and run the BatchUpdate test. On our iPhone 6 Plus, the one-at-a-time update takes about 0.07 seconds, while the batch update takes about 0.007 seconds. There are two things to notice.

· The batch update is about 10 times faster.

· The one-at-a-time update is still pretty fast.

We updated only 500 objects in our test, which the pre-iOS 8 approach handled in a few hundredths of a second. This tells us that we probably shouldn’t do all updates through the batch route, since we’re forsaking the validation that Core Data gives us to squeeze out a little performance. Instead, we should save batch updates for when we truly need them: when we must update tens of thousands (or more) of objects.

Analyzing Performance

Although thinking things through before writing code and understanding the implications of things such as faulting, prefetching, and memory usage usually nets you solid code that performs well, you often need a nonbiased, objective opinion on how your application is performing. The least biased and most objective opinion on your application’s performance comes from the computer it’s running on, so asking your computer to measure the results of your application’s Core Data interaction provides essential insight for optimizing performance.

Apple provides a tool called Instruments that allows you to measure several facets of an application, including Core Data–related items. This section shows how to use Instruments to measure the Core Data aspects of your application. We encourage you to explore the other measurements Instruments offers as well.

Launching Instruments

To launch Instruments and begin profiling your application, select Product image Profile from the Xcode menu. This launches Instruments and displays a dialog asking you what you want to profile, as Figure 9-8 shows.

image

Figure 9-8. Instruments asking you to choose a template

Select the Core Data template and click the Choose button. That opens the main Instruments window with the Core Data instruments in the left sidebar, as Figure 9-9 shows.

image

Figure 9-9. The main Instruments window with the Core Data instruments

Click the Record button at the top left of the Instruments window to launch the iOS Simulator (Instruments does not support profiling Core Data instruments on the device) and the PerformanceTuning application.

Understanding the Results

The Instruments window shows the Core Data measurements it’s tracking, which include the following:

· Core Data fetches

· Core Data cache misses

· Core Data saves

Run any of the tests in the PerformanceTuning application—say, Subquery—and wait for the test to complete. When the test finishes, click the Stop button in the upper left of Instruments to stop the application and stop recording Core Data measurements. You can then review the results from your test to see information about saves, fetches, faults, and cache misses. You can save the results to the file system for further review by selecting File image Save As from the menu. You reopen them in Instruments using the standard File image Open menu item.

Figure 9-10 shows the Instruments window after running Subquery. As Figure 9-10 shows, by selecting Core Data Fetches on the left, you can see the fetch counts and fetch durations, which are in microseconds, for the Core Data fetches. The code ran three fetch requests (Instruments lists the call for a fetch twice: once when the call starts and once when it finishes). The first request, which took 1,192 microseconds, fetched 500 people, which you can tell from the fetch entity, Person, and the fetch count, 500.

image

Figure 9-10. Results from Subquery

You can see the call tree for the fetch request by changing the drop-down in the middle of the window from Event List to Call Tree. You can reduce the navigation depth required to see the calls in your application’s code by checking the box next to Hide System Libraries on the right of the window. Figure 9-11 shows the call tree for the fetch request. Using the call tree, you can determine which parts of your code are fetching data from your Core Data store.

image

Figure 9-11. The call tree for the fetch requests

This is just a taste of what Instruments can do for you to help you determine how your application is using Core Data and how it can lead you to places where Core Data performance is slow enough to warrant optimization efforts. We encourage you to investigate the Instruments tool further, trying out the other Core Data instruments to see things like cache misses and saves so you can determine your application’s hot spots.

Dealing with Multiple Threads

There inevitably comes a time, when writing a non-trivial app, that you consider using multiple threads. Usually it is done for performance reasons—to offload tasks to the background—but the performance gains come at the expense of some complexity. This section shows you how to recognize some common problems that arise when using multiple threads with Core Data and how to solve them.

Thread Safety

Most of Apple’s application programming interfaces (APIs) aren’t thread safe unless they explicitly state that they are. Core Data, in true Apple fashion, is not thread safe. But what does this mean anyway? It means that the Core Data developers have not taken any precaution to ensure that your code won’t blow up if you let two threads share API resources. In our case, it means that you can’t safely share NSManagedObject, NSManagedContext, or any other Core Data object between threads.

Instead of thinking the non-thread safety reflects a lack of care or quality, look at it as delegating responsibility. Apple preferred focusing on keeping things simple and on maximizing performance. They delegate the responsibility of managing thread safety to you, the developer, whenever you need it.

Oh Come On, What Could Possibly Go Wrong?

It’s a common reflex among some developers to just dismiss complexity if the damage doesn’t appear to be immediate. Maybe you’re thinking that your threads are well behaved and well trained, and they won’t fight over resources. Maybe you’re thinking your app is not very complex so it should be OK to toss Core Data objects across threads. Or, maybe you’re simply feeling lucky. If any of this is true, then this section is for you.

Developers who have had to deal with live production applications over time often say that any scenario that can possibly happen will happen when the app is in the hands of real users. If two threads could potentially collide, they will. If you are not careful, you will have these concurrency problems. This section will show you how to recognize some of these problems.

In order to demonstrate thread concurrency issues, we build a simple app that writes a lot of data and also reports progress. One thought you might have is that we could write some data, report status, write some more, report more status, and so on. It’s true, we could perform writing and reading in serial chunks, but this would significantly increase the amount of time it takes to write your data and therefore deteriorate the user experience. By using threads, we can perform these tasks in parallel and keep the UI responsive.

Open Xcode and create a new application using the single-view application template. Call the application MultiThread. Our first step is to set up and initialize a Core Data stack. Create a new data model called MultiThread.xcdatamodeld. Add a single entity called MyData and give it a single attribute called myValue of type Integer 16. Your data model should match Figure 9-12.

image

Figure 9-12. The MultiThread data model

For clarity, we create a class that contains the Core Data setup. Create a new class called Persistence (a subclass of NSObject) and make Persistence.h look as shown in Listing 9-51.

Listing 9-51. Persistence.h

#import <Foundation/Foundation.h>
@import CoreData;

@interface Persistence : NSObject

@property (readonly, strong, nonatomic) NSManagedObjectModel *managedObjectModel;
@property (readonly, strong, nonatomic) NSPersistentStoreCoordinator *persistentStoreCoordinator;

- (NSManagedObjectContext*)createManagedObjectContext;

@end

This header file should look familiar, although you might notice that, instead of a managed object context property, we have a method that creates a managed object context. We do this for threading purposes, which we explain later in this chapter.

Next, open Persistence.m or Persistence.swift and implement the initialization in a manner similar to what we have done in previous chapters. Listing 9-52 shows the Objective-C version and Listing 9-53 shows the Swift version.

Listing 9-52. Persistence.m

- (instancetype)init {
self = [super init];
if (self != nil) {
// Initialize the managed object model
NSURL *modelURL = [[NSBundle mainBundle]
URLForResource:@"MultiThread" withExtension:@"momd"];
_managedObjectModel = [[NSManagedObjectModel alloc]
initWithContentsOfURL:modelURL];

// Initialize the persistent store coordinator
NSURL *storeURL = [[self applicationDocumentsDirectory]
URLByAppendingPathComponent:@"MultiThread.sqlite"];

// Delete the existing store if it exists so we can also start fresh
NSFileManager *fm = [NSFileManager defaultManager];
[fm removeItemAtURL:storeURL error:nil];

NSError *error = nil;
_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc]
initWithManagedObjectModel:self.managedObjectModel];
if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil
URL:storeURL
options:nil
error:&error]) {
NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
abort();
}
}
return self;
}

- (NSURL *)applicationDocumentsDirectory {
return [[[NSFileManager defaultManager]
URLsForDirectory:NSDocumentDirectory
inDomains:NSUserDomainMask] lastObject];
}

- (NSManagedObjectContext*)createManagedObjectContext {
NSManagedObjectContext *managedObjectContext =
[[NSManagedObjectContext alloc] init];
[managedObjectContext
setPersistentStoreCoordinator:self.persistentStoreCoordinator];

return managedObjectContext;
}

@end

Listing 9-53. Persistence.swift

import Foundation
import CoreData

class Persistence {
init() {
println("Pretending to do a migration");
NSThread.sleepForTimeInterval(10)
println("Done with pretending...");
}

// MARK: - Core Data stack

lazy var applicationDocumentsDirectory: NSURL = {
// The directory the application uses to store the Core Data store file. This code uses a directory named "book.persistence.MultiThreadSwift" in the application's documents Application Support directory.
let urls = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)
return urls[urls.count-1] as NSURL
}()

lazy var managedObjectModel: NSManagedObjectModel = {
// The managed object model for the application. This property is not optional. It is a fatal error for the application not to be able to find and load its model.
let modelURL = NSBundle.mainBundle().URLForResource("MultiThreadSwift", withExtension: "momd")!
return NSManagedObjectModel(contentsOfURL: modelURL)!
}()

lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator? = {
// The persistent store coordinator for the application. This implementation creates and return a coordinator, having added the store for the application to it. This property is optional since there are legitimate error conditions that could cause the creation of the store to fail.
// Create the coordinator and store
var coordinator: NSPersistentStoreCoordinator? = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel)
let url = self.applicationDocumentsDirectory.URLByAppendingPathComponent("MultiThreadSwift.sqlite")

// Delete the existing store if it exists so we can also start fresh
let fm = NSFileManager.defaultManager()
fm.removeItemAtURL(url, error: nil)

var error: NSError? = nil
var failureReason = "There was an error creating or loading the application's saved data."
if coordinator!.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: url, options: nil, error: &error) == nil {
coordinator = nil
// Report any error we got.
let dict = NSMutableDictionary()
dict[NSLocalizedDescriptionKey] = "Failed to initialize the application's saved data"
dict[NSLocalizedFailureReasonErrorKey] = failureReason
dict[NSUnderlyingErrorKey] = error
error = NSError(domain: "YOUR_ERROR_DOMAIN", code: 9999, userInfo: dict)
// Replace this with code to handle the error appropriately.
// abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
NSLog("Unresolved error \(error), \(error!.userInfo)")
abort()
}

return coordinator
}()

lazy var managedObjectContext: NSManagedObjectContext? = {
return self.createManagedObjectContext()
}()

func createManagedObjectContext() -> NSManagedObjectContext? {
// Returns the managed object context for the application (which is already bound to the persistent store coordinator for the application.) This property is optional since there are legitimate error conditions that could cause the creation of the context to fail.
let coordinator = self.persistentStoreCoordinator
if coordinator == nil {
return nil
}

var managedObjectContext = NSManagedObjectContext()
managedObjectContext.persistentStoreCoordinator = coordinator
return managedObjectContext
}

// MARK: - Core Data Saving support

func saveContext () {
if let moc = self.managedObjectContext {
var error: NSError? = nil
if moc.hasChanges && !moc.save(&error) {
// Replace this implementation with code to handle the error appropriately.
// abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
NSLog("Unresolved error \(error), \(error!.userInfo)")
abort()
}
}
}
}

Important Notice how the code deletes the SQLite file if it exists by calling NSFileManager’s removeItemAtURL:error: method. In a real-world app, this is obviously a bad practice as it removes all of the persisted data. In our case, it is very handy as it allows us to launch the app multiple times without having to worry about residual data.

Now that we are done with the Core Data setup code, we can move on to ViewController and experiment with the code to see what we can do with threads.

For the Objective-C version, edit ViewController.m, to add the appropriate import statements, private properties for Persistence and NSManagedObjectContext instances, and an initializer to run the setup code, as Listing 9-54 shows. For Swift, make similar edits toViewController.swift, as Listing 9-55 shows.

Listing 9-54. ViewController.m

@import CoreData;
#import "ViewController.h"
#import "Persistence.h"

@interface ViewController ()
@property (nonatomic, strong) Persistence *persistence;
@property (nonatomic, strong) NSManagedObjectContext *managedObjectContext;
@end

@implementation ViewController

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if(self) {
self.persistence = [[Persistence alloc] init];
self.managedObjectContext = [self.persistence createManagedObjectContext];
}

return self;
}

- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
}

- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}

@end

Listing 9-55. ViewController.swift

import UIKit
import CoreData

class ViewController: UIViewController {

var persistence: Persistence?
var managedObjectContext: NSManagedObjectContext?

required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)

self.persistence = Persistence()
self.managedObjectContext = self.persistence?.managedObjectContext
}

override func viewDidLoad() {
super.viewDidLoad()
}

override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}

At this point, you should be able to launch the app and see nothing but a blank screen.

Still in ViewController, we now create a new method for writing the data. In this method, we write 10,000 objects to the store. We save the context when we’re done writing so all the objects will be in the persistent store, so that any other managed object context will see them. Listing 9-56 shows the Objective-C version and Listing 9-57 shows the Swift version.

Listing 9-56. Writing 10,000 Objects (Objective-C)

- (void)writeData {
NSManagedObjectContext *context = self.managedObjectContext;

NSLog(@"Writing");
for(int i=0; i<10000; i++) {
NSManagedObject *obj = [NSEntityDescription
insertNewObjectForEntityForName:@"MyData"
inManagedObjectContext:context];
[obj setValue:[NSNumber numberWithInt:i] forKey:@"myValue"];
}

NSError *error;
[context save:&error];
if(error) {
NSLog(@"Boom while writing! %@", error);
}

NSLog(@"Done");
}

Listing 9-57. Writing 10,000 Objects (Swift)

func writeData() {
if let context = self.managedObjectContext {
println("Writing");
for var i=0; i<10000; i++ {
let obj = NSEntityDescription.insertNewObjectForEntityForName("MyData", inManagedObjectContext: context) as NSManagedObject
obj.setValue(Int(i), forKey: "myValue")
}

persistence?.saveContext()

println("Done")
}
else {
println("Missing context. Cannot write.")
}
}

Next, we add a method to report status—a method to read what we’ve written. It returns the fraction of the 10,000 objects that have been written. Listing 9-58 shows the Objective-C method and Listing 9-59 shows the Swift function.

Listing 9-58. Reading 10,000 Objects (Objective-C)

- (CGFloat)reportStatus {
NSManagedObjectContext *context = self.managedObjectContext;

NSFetchRequest *request = [[NSFetchRequest alloc] init];
request.entity = [NSEntityDescription entityForName:@"MyData"
inManagedObjectContext:context];

NSError *error;
NSUInteger count = [context countForFetchRequest:request error:&error];

if(error) {
NSLog(@"Boom while reading! %@", error);
}

return (CGFloat)count / 10000.0f;
}

Listing 9-59. Reading 10,000 Objects (Swift)

func reportStatus() -> Float {
if let context = self.managedObjectContext {
let request = NSFetchRequest(entityName: "MyData")
var error: NSError? = nil
let count = context.countForFetchRequest(request, error: &error)
if error != nil {
println("Error while reading")
}

return Float(count) / 10000.0
}
else {
println("Missing context. Cannot report status")
return 0
}
}

Now that we can read and write objects, we just have to call the code to do that. We will do this in the viewDidAppear: method as shown in Listing 9-60 (Objective-C) and Listing 9-61 (Swift).

Listing 9-60. Calling the Writing and Reading Methods (Objective-C)

- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];

[self writeData];

CGFloat status = 0;
while(status < 1.0) {
status = [self reportStatus];
NSLog(@"Status: %lu%%", (unsigned long)(status * 100));
}

NSLog(@"All done");
}

Listing 9-61. Calling the Writing and Reading Methods (Swift)

override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)

self.writeData()

var status : Float = 0
while (status < 1) {
status = self.reportStatus()
println(NSString(format: "Status: %lu%%", Int(status * 100)))
}
println("All done")
}

Obviously, if you launch the app at this point, all the writing will occur before any progress can be reported. Once status is reported, the only one you will see is 100%. Try it.

09:35:11.586 MultiThread[3246:60b] Writing
09:35:11.738 MultiThread[3246:60b] Done
09:35:11.753 MultiThread[3246:60b] Status: 100%
09:35:11.754 MultiThread[3246:60b] All done

Surely you already know what happened. Since we’re running everything on the same thread, writing the data happens first, and only after the write completes do we start reporting status. We could flip this around and start reporting status before writing the data, but then the writing section would never happen since the status loop would iterate forever.

This is a case for multiple threads. The solution is to dispatch each of the two actions (writing and reporting progress) to their own threads. Edit viewDidAppear: to use multiple threads using Grand Central Dispatch. One thread will write the data, and the other thread will report status by reading the data. Both threads will run asynchronously, allowing us to write the data while reporting on the progress of those writes. Listing 9-62 shows the Objective-C version, and Listing 9-63 shows the Swift version.

Listing 9-62. Writing and Reading on Separate Threads (Objective-C)

- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self writeData];
});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
CGFloat status = 0;
while(status < 1.0) {
status = [self reportStatus];
NSLog(@"Status: %lu%%", (unsigned long)(status * 100));
}

NSLog(@"All done");
});
}

Listing 9-63. Writing and Reading on Separate Threads (Swift)

override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), { () -> Void in
self.writeData()
})

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), { () -> Void in
var status : Float = 0
while (status < 1) {
status = self.reportStatus()
println(NSString(format: "Status: %lu%%", Int(status * 100)))
}
println("All done")
})
}

Now, when the app runs, both tasks will happen simultaneously. So you were feeling lucky, huh? Go ahead and try your luck by launching the app.

Odds are, you got something like the following (if you didn’t, we’d like you to help us pick lottery numbers):

09:40:03.120 MultiThread[3282:1303] Writing
09:40:03.122 MultiThread[3282:3603] Status: 0%
09:40:03.124 MultiThread[3282:3603] *** Terminating app due to uncaught exception 'NSGenericException', reason: '*** Collection <__NSCFSet: 0x1096122b0> was mutated while being enumerated.'
*** First throw call stack:
(
0 CoreFoundation 0x000000010194a495 __exceptionPreprocess + 165
1 libobjc.A.dylib 0x00000001016a999e objc_exception_throw + 43
2 CoreFoundation 0x00000001019ce68e __NSFastEnumerationMutationHandler + 126
3 CoreData 0x0000000101c2666b -[NSManagedObjectContext(_NSInternalAdditions) _countWithNoChangesForRequest:error:] + 1675
4 CoreData 0x0000000101c25d5b -[NSManagedObjectContext countForFetchRequest:error:] + 1211
5 MultiThread 0x00000001000022ae -[ViewController reportStatus] + 286
6 MultiThread 0x0000000100001ed9 __32-[ViewController viewDidAppear:]_block_invoke17 + 89
7 libdispatch.dylib 0x00000001020e7851 _dispatch_call_block_and_release + 12
8 libdispatch.dylib 0x00000001020fa72d _dispatch_client_callout + 8
9 libdispatch.dylib 0x00000001020eab27 _dispatch_root_queue_drain + 380
10 libdispatch.dylib 0x00000001020ead12 _dispatch_worker_thread2 + 40
11 libsystem_pthread.dylib 0x0000000102447ef8 _pthread_wqthread + 314
12 libsystem_pthread.dylib 0x000000010244afb9 start_wqthread + 13
)
libc++abi.dylib: terminating with uncaught exception of type NSException

The problem here is that we’re peeking at the context’s cache while it’s writing to it. This isn’t necessarily a Core Data issue. You cannot iterate through a collection and peek into it at the same time from two different threads. So it’s definitely a thread safety issue. This experiment exposes the cardinal rule when using multiple threads with Core Data: give each thread its own managed object context. Remember the createManagedObjectContext method we created? We use that to give the writeData method its own managed object context, rather than using the managed object context that the reportStatus method uses.

When inmates fight, they go into confinement. They are stripped from their reason to fight because they have nothing to fight over and nobody to fight with. Thread confinement is a common strategy to deal with threads not “playing nice” with each other.

Things go wrong pretty fast when using Core Data with multiple threads. Managing concurrency is done simply by avoiding sharing the resources threads fight over. It bears repeating: the most common solution is to create a context for each thread.

Important Each context must be created by the thread that is going to use it.

Another change we make to this code is to save the managed object context after we add each object. We do this so that the context’s cache is written through to the persistent store, so that the managed object context used in the reportStatus method sees the objects. If we waited until all 10,000 objects were written before saving the context, the reportStatus method would report a 0% status until the writeData thread finishes writing, and then reportStatus would jump directly to 100%.

Listing 9-64 shows the updated Objective-C version, and Listing 9-65 shows the updated Swift version.

Listing 9-64. Saving the Context After Every Write (Objective-C)

- (void)writeData {
NSManagedObjectContext *context = [self.persistence createManagedObjectContext];

NSLog(@"Writing");
for(int i=0; i<10000; i++) {
NSManagedObject *obj = [NSEntityDescription insertNewObjectForEntityForName:@"MyData" inManagedObjectContext:context];
[obj setValue:[NSNumber numberWithInt:i] forKey:@"myValue"];

NSError *error;
[context save:&error];
if(error) {
NSLog(@"Boom while writing! %@", error);
}
}

NSLog(@"Done");
}

Listing 9-65. Saving the Context After Every Write (Swift)

func writeData () {
if let context = self.persistence?.createManagedObjectContext() {
println("Writing");
for var i=0; i<10000; i++ {
let obj = NSEntityDescription.insertNewObjectForEntityForName("MyData", inManagedObjectContext: context) as NSManagedObject
obj.setValue(Int(i), forKey: "myValue")

context.save(nil)
}

println("Done")
}
else {
println("Missing context. Cannot write.")
}
}

If you build and run the application now, you’ll see the status percentage in the console steadily climb until it reaches 100% and then the “All done” message appears.

Gratuitous User Interface

Since we’ve managed to make our app multithreaded, let’s finish the work and put a progress bar on our UI so we can examine how to display progress in a way that users can see.

Open Main.storyboard and add a progress bar and a label to the view, as Figure 9-13 shows. The label will display a textual message indicating the progress, and the progress bar will graphically reflect the same progress.

image

Figure 9-13. The layout of the progress bar and label

Now create two IBOutlets, either in ViewController.m (Objective-C) or in ViewController.swift (Swift), called label and progressView. Connect them in Interface Builder to the label and progress bar you just added. Listing 9-66 shows what to add toViewController.h, and Listing 9-67 shows what to add to ViewController.swift.

Listing 9-66. Adding Outlets to ViewController.m

@interface ViewController ()
@property (nonatomic, strong) Persistence *persistence;
@property (nonatomic, strong) NSManagedObjectContext *managedObjectContext;
@property (nonatomic, weak) IBOutlet UILabel *label;
@property (nonatomic, weak) IBOutlet UIProgressView *progressView;
@end

Listing 9-67. Adding Outlets to ViewController.swift

@IBOutlet weak var label: UILabel!
@IBOutlet weak var progressView: UIProgressView!

Now update viewDidAppear: to reflect the report status to the UI. We set the label to the textual version of the status, and we move the progress bar along appropriately. Since we’re calling the reportStatus method from a thread that’s not the main thread—the thread used by the UI—we need to make sure we dispatch updates to the main queue in order for them to be executed by the main thread. All interface updates should be done from the main thread. It’s not just in the Core Data arena that proper thread handling is important!

Listing 9-68 shows the Objective-C version of the code, and Listing 9-69 shows the Swift version.

Listing 9-68. Updating the UI to Show Progress (Objective-C)

-(void)viewDidAppear:(BOOL)animated {
[super viewDidAppear: animated];

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self writeData];
});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
CGFloat status = 0;
while(status < 1.0) {
status = [self reportStatus];
dispatch_async(dispatch_get_main_queue(), ^{
self.progressView.progress = status;
self.label.text = [NSString stringWithFormat:@"%lu%%",
(unsigned long)(status * 100)];
});
}

NSLog(@"All done");
});
}

Listing 9-69. Updating the UI to Show Progress (Swift)

override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), { () -> Void in
self.writeData()
})

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), { () -> Void in
var status : Float = 0
while (status < 1) {
status = self.reportStatus()
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.progressView.progress = status
self.label.text = NSString(format: "Status: %lu%%", Int(status * 100))
})
}
println("All done")
})
}

Now launch the app again and watch the UI update with the textual percentage and the moving progress bar as the write operation progresses to completion.

Initializing the Stack on a Different Thread

It’s a good practice to consider initializing your Core Data stack on a thread other than the main thread. While initializing Core Data is typically fast, you will find that it may take a long time in some cases, especially if there is a large store migration involved. If you initialize inapplication:didFinishLaunchingWithOptions: and it takes a while, the app will crash. If you initialized in a controller like we did, the UI will become unresponsive. Either way, it’s bad. For this reason, we encourage you to consider initializing on a different thread.

Let’s put all of this into practice with our MultiThread app. First, we’ll simulate a long start by adding a 10-second pause. Open Persistence.m or Persistence.swift and add the code shown in Listing 9-70 to the end of the if block in the init method (Objective-C), or create theinit method shown in Listing 9-71 (Swift).

Listing 9-70. Faking a 10-Second Migration (Objective-C)

NSLog(@"Pretending to do a migration");
[NSThread sleepForTimeInterval:10];
NSLog(@"Done with pretending...");

Listing 9-71. Faking a 10-Second Migration (Swift)

init() {
println("Pretending to do a migration");
NSThread.sleepForTimeInterval(10)
println("Done with pretending...");
}

Now launch the app and notice how long the splash screen shows before the interface appears, leaving your user to wonder what might be happening.

Let’s address this by launching the Core Data stack initialization on a different thread in the initWithCoder: method of ViewController as shown in Listing 9-72 (Objective-C) or Listing 9-73 (Swift).

Listing 9-72. Initializing Core Data on a Separate Thread (Objective-C)

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if(self) {
[self addObserver:self forKeyPath:@"managedObjectContext" options:NSKeyValueObservingOptionNew context:NULL];

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
self.persistence = [[Persistence alloc] init];
dispatch_async(dispatch_get_main_queue(), ^{
self.managedObjectContext = [self.persistence createManagedObjectContext];
});
});
}

return self;
}

Listing 9-73. Initializing Core Data on a Separate Thread (Swift)

required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), { () -> Void in
self.persistence = Persistence()
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.managedObjectContext = self.persistence?.managedObjectContext
})
})
}

A couple of things are happening here. First, we add an observer (using the Key-Value Observer (KVO) API) of the managedObjectContext property in the Objective-C version of the code. Because everything will happen on a different thread, we can no longer count on this value being set in viewDidAppear:, so we’ll launch our "write" code when we get notified instead. We then dispatch the initialization code to another thread. When Core Data finishes initializing, we set the managedObjectContext property on the main thread. Why do we need to set it on the main thread? Because as we stated before, the managed context must be created on the thread that will use it. Since we will be using the managed object on the main thread, we set it on the main thread.

When we set the managed object context property value, the KVO notification will fire so we also need to observe it and launch the "write" code when the property changes. We do this differently for Objective-C than for Swift, since the semantics of these languages differ. For Objective-C, move all your code from viewDidAppear: to a new method called observeValueForKeyPath:ofObject:change:context:, which will be automatically called when the managedObjectContext property that we’re observing changes. Listing 9-74 shows the code.

Listing 9-74. Observing the Change in managedObjectContext (Objective-C)

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self writeData];
});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
CGFloat status = 0;
while(status < 1.0) {
status = [self reportStatus];
dispatch_async(dispatch_get_main_queue(), ^{
self.progressView.progress = status;
self.label.text = [NSString stringWithFormat:@"%lu%%",
(unsigned long)(status * 100)];
});
}

NSLog(@"All done");
});
}

For Swift, we create a didSet observer for the managedObjectContext property that calls a new method we create called initiate. Listing 9-75 shows the code.

Listing 9-75. Observing the Change in managedObjectContext (Swift)

var managedObjectContext: NSManagedObjectContext? {
didSet {
self.initiate()
}
}

func initiate() {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), { () -> Void in
self.writeData()
})

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), { () -> Void in
var status : Float = 0
while(status < 1) {
status = self.reportStatus()
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.progressView.progress = status
self.label.text = NSString(format: "Status: %lu%%", Int(status * 100))
})

//println(NSString(format: "Status: %lu%%", Int(status * 100)))
}

println("All done")
})
}

In both languages, the viewDidAppear method should now be gone from your code. The last step, to keep everything clean, is to make sure we have default values for the UI elements. We set initial values in viewWillAppear:, as shown in Listing 9-76 (Objective-C) and Listing 9-77(Swift).

Listing 9-76. Setting the UI Elements to Default Values (Objective-C)

- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];

self.progressView.progress = 0;
self.label.text = @"Initializing Core Data";
}

Listing 9-77. Setting the UI Elements to Default Values (Swift)

override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)

self.progressView.progress = 0
self.label.text = "Initializing Core Data"
}

Now launch the application and notice how the splash screen displays only briefly before the application UI appears and displays “Initializing Core Data” for about 10 seconds, while our fake migration occurs, and then the progress marches from 0% to 100%.

Summary

From uniquing to faulting to cachingmanaged objects, Core Data performs a significant amount of data access performance optimization for you. These optimizations come free, without any extra effort on your part. You should be aware of the optimizations that Core Data provides, however, so that you make sure to work with, not against, them.

Not all Core Data performance gains come automatically, however. In this chapter, you learned how to use techniques such as prefetching and predicate optimization to squeeze all the performance from Core Data that you can for your applications. You also learned how to analyze your Core Data application using the Instruments application, so you can understand how your application is using Core Data and where the trouble spots are. Finally, you learned how to use multiple threads to improve the responsiveness and perceived performance of your applications.

Other iOS programming books might do an excellent job showing you how to display a spinner and a “Please Wait” message when running long queries against your persistent store. This book shows you how to avoid the need for the spinner and “Please Wait” message entirely.