Instruments Tutorial with Swift: Getting Started

In this Xcode tutorial, you’ll learn how to use Instruments to profile and debug performance, memory and reference issues in your iOS apps. By Lea Marolt Sonnenschein.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 5 of this article. Click here to view the first page.

Drilling Deep

Perform an image search and drill into the results.

Scroll up and down the list a few times so that you’ve got a good amount of data in Time Profiler. Notice the numbers in the middle of the screen changing and the graph filling in. This tells you the app is using CPU cycles.

It may look subtle in the simulator, but notice the choppiness and how the scrolling staggers as you swipe up and and down. No collection view is ready to ship until it scrolls like butter!

To help pinpoint the problem, you’ll set some options. Click Stop, and below the detail panel, click Call Tree. In the popover that appears, select Separate by Thread, Invert Call Tree and Hide System Libraries.

Time Profiler Call Tree Settings

Here’s what each option is doing to the data displayed in Detail panel:

  • Separate by State: This option groups results by your app’s lifecycle state and is a useful way to inspect how much work your app is doing and when.
  • Separate by Thread: This separates the threads and enables you to understand which threads are responsible for the greatest amount of CPU use.
  • Invert Call Tree: This option shows the most recent frame first in the stack trace.
  • Hide System Libraries: When you select this option, you’ll only see symbols from your own app. It’s often useful to select this option since you can’t do much about how much CPU the system libraries are using.
  • Flatten Recursion: This option shows recursive functions — which are functions that call themselves — with one entry in each stack trace, rather than multiple times.
  • Top Functions: Enabling this makes Instruments consider the total time spent in a function as the sum of the time within that function, as well as the time spent in functions called by that function. So if function A calls B, then Instruments reports A’s time as the time spent in A plus the time spent in B. This can be useful, as it lets you pick the largest time figure each time you descend into the call stack, zeroing in on your most time-consuming methods.

Scan the results to identify which rows have the highest percentage in the Weight column. The row with the Main Thread is using up a significant proportion of CPU cycles. Unfold this row by clicking the small arrow to the left of the text and keep looking until you see one of your own methods marked with the “person” symbol. Although some values may be different, the order of the entries should be similar to what’s shown in the table below:

Call Tree Results

Well, that doesn’t look good. The app spends the vast majority of time creating a UIImage with a “tonal” filter for the thumbnail photos. That shouldn’t come as too much of a shock to you, as the table loading and scrolling were the clunkiest parts of the UI, and that’s when the table cells were constantly being updated.

To find out more about what’s going on within that method, double-click its row in the table to pull up the following view:

Time Profiler Code

withTonalFilter is a property extension on UIImage, and it spends a lot of time invoking the method that creates the CGImage output after applying the image filter.

There’s not much you can do to speed this up. Creating the image is an intensive process and takes as long as it takes. Try stepping back to see where the app calls withTonalFilter. Click Root in the breadcrumb trail at the top of the code view to get back to the previous screen:

Call Tree Breadcrumb

Now click the small arrow to the left of the withTonalFilter row at the top of the table. This will show the caller of withTonalFilter — you may need to unfold the next row too. When profiling Swift, there will sometimes be duplicate rows in Call Tree which are prefixed with @objc. You’re interested in the first row that’s prefixed with the “person” icon, which indicates it belongs to your app’s target:

Tonal Filter Call Tree

In this case, this row refers to (_:cellForItemAt:) in SearchResultsViewController.swift. Double-click the row to see the associated code from the project.

Code for Cell For Item At, Line 71

Now you can see what the problem is. Take a look at line 71: Creating a UIImage with the tonal filter takes a long time to execute, and you create it from collectionView(_:cellForItemAt:).

Offloading the Work

To solve this, you’ll do two things: First, offload the image filtering onto a background thread with DispatchQueue.global().async. Second, cache each image after it’s generated. There’s a small, simple image caching class — with the catchy name ImageCache — included in the starter project. It stores and retrieves images in memory with a given key.

You could now switch to Xcode and manually find the source file you’re looking at in Instruments. But there’s a handy Open in Xcode button in Instruments. Locate it in the panel above the code and click it:

Open In Xcode Button

There you go! Xcode opens at exactly the right place. Boom!

Now, within collectionView(_:cellForItemAt:), replace the call to loadThumbnail(for:completion:) with the following:

ImageCache.shared.loadThumbnail(for: flickrPhoto) { result in
  switch result {
  case .success(let image):
    if resultsCell.flickrPhoto == flickrPhoto {
      if flickrPhoto.isFavorite {
        resultsCell.imageView.image = image
      } else {
        // 1
        if let cachedImage =
          ImageCache.shared.image(forKey: "\(flickrPhoto.id)-filtered") {
          resultsCell.imageView.image = cachedImage
        } else {
          // 2
          DispatchQueue.global().async {
            if let filteredImage = image.withTonalFilter {
              ImageCache.shared
                .set(filteredImage, forKey: "\(flickrPhoto.id)-filtered")

              DispatchQueue.main.async {
                resultsCell.imageView.image = filteredImage
              }
            }
          }
        }
      }
    }
  case .failure(let error):
    print("Error: \(error)")
  }
}

The first section of this code is the same as it was before and loads the Flickr photo’s thumbnail image from the web. If the photo is a favorite, the cell displays the thumbnail without modification. However, if the photo isn’t a favorite, it applies the tonal filter.

This is where you change things:

  1. Check to see if a filtered image for this photo exists in the image cache. If yes, display that image.
  2. If not, dispatch the call to create an image with the tonal filter onto a background queue. This allows the UI to remain responsive while the filter runs. When the filter completes, save the image in the cache and update the image view on the main queue.

This takes care of the filtered images, but there are still the original Flickr thumbnails to resolve. Open Cache.swift and find loadThumbnail(for:completion:). Replace it with the following:

func loadThumbnail(
  for photo: FlickrPhoto, 
  completion: @escaping FlickrAPI.FetchImageCompletion
) {
  if let image = ImageCache.shared.image(forKey: photo.id) {
    completion(Result.success(image))
  } else {
    FlickrAPI.loadImage(for: photo, withSize: "m") { result in
      if case .success(let image) = result {
        ImageCache.shared.set(image, forKey: photo.id)
      }
      completion(result)
    }
  }
}

This is similar to how you handled filtered images. If an image already exists in the cache, call the completion closure straight away with the cached image. Otherwise, load the image from Flickr and store it in the cache.

Press Command-I to run the app in Instruments again. Notice this time Xcode doesn’t ask you which instrument to use. This is because you still have a window open for your app, and Instruments assumes you want to run again with the same options.

Perform a few more searches. The UI is not as clunky now! The app now applies the image filter in the background and caches the result, so images only have to be filtered once. You’ll see many dispatch_worker_threads in the Call Tree. These handle the heavy lifting of applying image filters.

Looks great! Is it time to ship it? Not yet!