Chapters

Hide chapters

Core Data by Tutorials

Seventh Edition · iOS 13 · Swift 5.2 · Xcode 11

Before You Begin

Section 0: 14 chapters
Show chapters Hide chapters

8. Measuring & Boosting Performance
Written by Matthew Morey

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

In many ways, it’s a no-brainer: You should strive to optimize the performance of any app you develop. An app with poor performance will, at best, receive bad reviews and, at worst, become unresponsive and crash.

This is no less true of apps that use Core Data. Luckily, most implementations of Core Data are fast and light already, due to Core Data’s built-in optimizations, such as faulting.

However, the flexibility that makes Core Data a great tool means you can use it in ways that negatively impact performance. From poor choices in setting up the data model to inefficient fetching and searching, there are many opportunities for Core Data to slow down your app.

You’ll begin the chapter with an app that’s a slow-working memory hog. By the end of the chapter, you’ll have an app that’s light and fast, and you’ll know exactly where to look and what to do if you find yourself with your own heavy, sluggish app — and how to avoid that situation in the first place!

Getting started

As with most things, performance is a balance between memory and speed. Your app’s Core Data model objects can exist in two places: in random access memory (RAM) or on disk.

Accessing data in RAM is much faster than accessing data on disk, but devices have much less RAM than disk space.

iOS devices, in particular, have less available RAM, which prevents you from loading tons of data into memory. With fewer model objects in RAM, your app’s operations will be slower due to frequent slow disk access. As you load more model objects into RAM, your app will probably feel more responsive, but you can’t starve out other apps or the OS will terminate your app!

The starter project

The starter project, EmployeeDirectory, is a tab bar-based app full of employee information. It’s like the Contacts app, but for a single fictional company.

Measure, change, verify

Instead of guessing where your performance bottlenecks are, you can save yourself time and effort by first measuring your app’s performance in targeted ways. Xcode provides tools just for this purpose.

Measuring the problem

Build, run, and wait for the app to launch. Once it does, use the Memory Report to view how much RAM the app is using.

Exploring the data source

In Xcode, open the project navigator and click on EmployeeDirectory.xcdatamodeld to view the data model. The model for the starter project consists of an Employee entity with 11 attributes and a Sale entity with two attributes.

{
  "guid": "769adb89-82ad-4b39-be41-d02b89de7b94",
  "active": true,
  "picture": "face10.jpg",
  "name": "Kasey Mcfarland",
  "vacationDays": 2,
  "department": "Marketing",
  "startDate": "1979-09-05",
  "email": "kaseymcfarland@liquicom.com",
  "phone": "+1 (909) 561-2981",
  "address": "201 Lancaster Avenue, West Virginia, 2583",
  "about": "Dolore reprehenderit ... voluptate consectetur.\r\n"
},

Making changes to improve performance

The likely culprit for the high memory usage is the employee profile picture. Since the picture is stored as a binary data attribute, Core Data will allocate memory and load the entire picture when you access an employee record — even if you only need to access the employee’s name or email address!

import Foundation
import CoreData

public class EmployeePicture: NSManagedObject {
}

extension EmployeePicture {
  @nonobjc public class func fetchRequest() ->
    NSFetchRequest<EmployeePicture> {
    return NSFetchRequest<EmployeePicture>(
      entityName: "EmployeePicture")
  }

  @NSManaged public var picture: Data?
  @NSManaged public var employee: Employee?
}
@NSManaged public var about: String?
@NSManaged public var active: NSNumber?
@NSManaged public var address: String?
@NSManaged public var department: String?
@NSManaged public var email: String?
@NSManaged public var guid: String?
@NSManaged public var name: String?
@NSManaged public var phone: String?
@NSManaged public var pictureThumbnail: Data?
@NSManaged public var picture: EmployeePicture?
@NSManaged public var startDate: Date?
@NSManaged public var vacationDays: NSNumber?
@NSManaged public var sales: NSSet?
cell.pictureImageView.image = UIImage(data: employee.picture!)
cell.pictureImageView.image =
  UIImage(data: employee.pictureThumbnail!)
let image = UIImage(data: employee.picture!)
headShotImageView.image = image
let image = UIImage(data: employee.pictureThumbnail!)
headShotImageView.image = image
guard let employeePicture = employee?.picture else {
  return
}
guard let employeePicture = employee?.picture?.picture else {
  return
}
employee.picture = pictureData
employee.pictureThumbnail =
  imageDataScaledToHeight(pictureData, height: 120)

let pictureObject =
  EmployeePicture(context: coreDataStack.mainContext)

pictureObject.picture = pictureData

employee.picture = pictureObject

Verify the changes

Now that you’ve made all the necessary changes to the project, it’s time to see if you actually improved the app.

Fetching and performance

Core Data is the keeper of your app’s data. Anytime you want to access the data, you have to retrieve it with a fetch request.

Fetch batch size

Core Data fetch requests include the fetchBatchSize property, which makes it easy to fetch just enough data, but not too much.

Measuring the problem

You’ll use the Instruments tool to analyze where the fetch operations are in your app.

Changes to improve performance

Open EmployeeListViewController.swift and find the following line of code in employeeFetchRequest(_:):

let fetchRequest: NSFetchRequest<Employee> =
  Employee.fetchRequest()

let fetchRequest: NSFetchRequest<Employee> =
  Employee.fetchRequest()
fetchRequest.fetchBatchSize = 10

Verify the changes

Now that you’ve made the necessary change to the project, it’s once again time to see if you’ve actually improved the app.

Advanced fetching

Fetch requests use predicates to limit the amount of data returned. As mentioned above, for optimal performance, you should limit the amount of data you fetch to the minimum needed: the more data you fetch, the longer the fetch will take.

Measure the problem

Instead of Instruments, you’ll use the XCTest framework to measure the performance of the department list screen. XCTest is usually used for unit tests, but it also contains useful tools for testing performance.

//1
let fetchRequest: NSFetchRequest<Employee> = Employee.fetchRequest()

var fetchResults: [Employee] = []
do {
  fetchResults = try coreDataStack.mainContext.fetch(fetchRequest)
} catch let error as NSError {
  print("ERROR: \(error.localizedDescription)")
  return [[String: String]]()
}

//2
var uniqueDepartments: [String: Int] = [:]
for employee in fetchResults where employee.department != nil {      
  uniqueDepartments[employee.department!, default: 0] += 1
}

//3
return uniqueDepartments.map { (department, headCount) in
  ["department": department,
   "headCount": String(headCount)]
}
func testTotalEmployeesPerDepartment() {
  measureMetrics([.wallClockTime],
                 automaticallyStartMeasuring: false) {

    let departmentList = DepartmentListViewController()
    departmentList.coreDataStack =
      CoreDataStack(modelName: "EmployeeDirectory")

    startMeasuring()
    _ = departmentList.totalEmployeesPerDepartment()
    stopMeasuring()
  }
}

Changes to improve performance

The current implementation of totalEmployeesPerDepartment uses a fetch request to iterate through all employee records. Remember the very first optimization in this chapter, where you split out the full-size photo into a separate entity? There’s a similar issue here: Core Data loads the entire employee record, but all you really need is a count of employees by department.

func totalEmployeesPerDepartmentFast() -> [[String: String]] {
  //1
  let expressionDescription = NSExpressionDescription()
  expressionDescription.name = "headCount"

  //2
  let arguments = [NSExpression(forKeyPath: "department")]
  expressionDescription.expression =
    NSExpression(forFunction: "count:",
                 arguments: arguments)

  //3
  let fetchRequest: NSFetchRequest<NSDictionary> =
    NSFetchRequest(entityName: "Employee")
  fetchRequest.propertiesToFetch =
    ["department", expressionDescription]
  fetchRequest.propertiesToGroupBy = ["department"]
  fetchRequest.resultType = .dictionaryResultType

  //4
  var fetchResults: [NSDictionary] = []
  do {
    fetchResults =
      try coreDataStack.mainContext.fetch(fetchRequest)
  } catch let error as NSError {
    print("ERROR: \(error.localizedDescription)")
    return [[String: String]]()
  }
  return fetchResults as! [[String: String]]
}
items = totalEmployeesPerDepartment()
items = totalEmployeesPerDepartmentFast()

Verify the changes

Now that you’ve made all the necessary changes to the project, it’s once again time to see if you’ve improved the app’s performance.

func testTotalEmployeesPerDepartmentFast() {
  measureMetrics([.wallClockTime],
                 automaticallyStartMeasuring: false) {
    let departmentList = DepartmentListViewController()
    departmentList.coreDataStack =
      CoreDataStack(modelName: "EmployeeDirectory")

    startMeasuring()
    _ = departmentList.totalEmployeesPerDepartmentFast()
    stopMeasuring()
  }
}

Fetching counts

As you’ve already seen, your app doesn’t always need all information from your Core Data objects; some screens simply need the counts of objects that have certain attributes.

Measure the problem

You’ll use XCTest again to measure the performance of the employee detail screen.

func salesCountForEmployee(_ employee: Employee) -> String {

  let fetchRequest: NSFetchRequest<Sale> = Sale.fetchRequest()
  fetchRequest.predicate = NSPredicate(
    format: "%K = %@",
    argumentArray: [#keyPath(Sale.employee), employee])

  let context = employee.managedObjectContext!
  do {
    let results = try context.fetch(fetchRequest)
    return "\(results.count)"
  } catch let error as NSError {
    print("Error: \(error.localizedDescription)")
    return "0"
  }
}
func testCountSales() {
  measureMetrics([.wallClockTime],
                 automaticallyStartMeasuring: false) {

    let employee = getEmployee()
    let employeeDetails = EmployeeDetailViewController()
    startMeasuring()
    _ = employeeDetails.salesCountForEmployee(employee)
    stopMeasuring()
  }
}

Changes to improve performance

In the previous example, you used NSExpression to group the data and provide a count of employees by department instead of returning the actual records themselves. You’ll do the same thing here.

func salesCountForEmployeeFast(_ employee: Employee) -> String {

  let fetchRequest: NSFetchRequest<Sale> = Sale.fetchRequest()
  fetchRequest.predicate = NSPredicate(
    format: "%K = %@",
    argumentArray: [#keyPath(Sale.employee), employee])

  let context = employee.managedObjectContext!

  do {
    let results = try context.count(for: fetchRequest)
    return "\(results)"
  } catch let error as NSError {
    print("Error: \(error.localizedDescription)")
    return "0"
  }
}
salesCountLabel.text = salesCountForEmployee(employee)
salesCountLabel.text = salesCountForEmployeeFast(employee)

Verify the changes

Now that you’ve made the necessary changes to the project, it’s once again time to see if you’ve improved the app. Open EmployeeDetailViewControllerTests.swift and add a new function to test the salesCountForEmployeeFast function you just created.

func testCountSalesFast() {
  measureMetrics([.wallClockTime],
                 automaticallyStartMeasuring: false) {

    let employee = getEmployee()
    let employeeDetails = EmployeeDetailViewController()
    startMeasuring()
    _ = employeeDetails.salesCountForEmployeeFast(employee)
    stopMeasuring()
  }
}

Using relationships

The code above is fast, but the faster method still seems like a lot of work. You have to create a fetch request, create a predicate, get a reference to the context, execute the fetch request and get the results out.

func salesCountForEmployeeSimple(_ employee: Employee)
                                 -> String {
  return "\(employee.sales!.count)"
}

Key points

  • Most implementations of Core Data are fast and light already, due to Core Data’s built-in optimizations, such as faulting.
  • When making improvements to Core Data performance you should measure, make targeted changes and then measure again to validate your changes had the intended impact.
  • Small changes to the data model, such as moving large binary blobs to other entities, can improve performance.
  • For optimal performance, you should limit the amount of data you fetch to the minimum needed: the more data you fetch, the longer the fetch will take.
  • Performance is a balance between memory and speed. When using Core Data in your apps, always keep this balance in mind.

Challenge

Using the techniques you just learned, try to improve the performance of the DepartmentDetailsViewController class. Don’t forget to write tests to measure the before and after execution times. As a hint, there are many methods that provide counts, rather than the full records; these can probably be optimized somehow to avoid loading the contents of the records.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now