Chapters

Hide chapters

Expert Swift

First Edition · iOS 14 · Swift 5.4 · Xcode 12.5

13. Instrumentation
Written by Ehab Amer

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

Being a great iOS software engineer isn’t only about being a grandmaster of the Swift language. It’s also about knowing which tools the platform puts at your disposal, how to use them to sharpen your skills and how to identify areas of improvement in your code.

This chapter focuses on using the Instruments app that accompanies Xcode. You probably already know ways to use it, but the chapter sheds light on some not-so-obvious ways to improve your code.

As you read along, you’ll cover some interesting topics:

  • Common memory issues and how to resolve them.
  • How to measure performance and identify bottlenecks in your code.
  • How to measure and improve the performance of CoreData.
  • How to identify lines in your code that take too much time to build.
  • An interesting instrument called Timelane, which you can install as a plugin to Instruments.
  • How you can use os_signpost to send your information on Instruments.

Getting started

In the starter folder, you’ll find multiple projects. You’ll use them all throughout this chapter. To begin, open the project “TheExhibition”. Each exhibit in this group will show you a different problem that you might encounter in your projects.

Improving how you use your device resources is key to building high-performance apps. For the first section, you’ll cover memory.

Memory optimization

The first exhibit, A-1: Optimization, is a gallery of wonderful images from NASA.

Calculating memory usage for images

The space that images take up in memory isn’t equal to their file size. Rather, images allocate memory that fits their resolution size. An example:

The solution

Now that you’ve established that the problem is caused by the high resolution of the photos, you’ll reduce their size to something more friendly to the gallery screen.

Memory leaks

Handling leaks is like the ABC’s of memory management. “Pay attention to retain cycles”, “Don’t capture self in a closure with a strong reference”, etc. So what’s new to say about the issue?

override func viewDidLoad() {
  super.viewDidLoad()
  infoWriter = InformationWriter(writer: self)
  infoWriter?.doSomething()
}
protocol WriterProtocol {
  func writeText(_ text: String)
}
init(writer: WriterProtocol) {
  writeOperation = { info in
    writer.writeText(info)
  }
}

Memory graph is conservative

The memory graph isn’t part of the Swift language. It’s a tool that analyzes the app in memory during runtime. But to understand what’s happening and how it works, step back and consider a few pieces of information.

Fixing the leak

Now, it’s time to fix the leak. In InformationWriter.swift, update the creation of the closure to the following:

writeOperation = { [weak writer] info in
  writer?.writeText(info)
}
protocol WriterProtocol: AnyObject

Performance

B-1 Time profiler, the 3rd cell in The Exhibition app, is a collection view of random numbers along with how many times that number was generated. It doesn’t do anything fancy. But the more you scroll in this screen, the more stuttering you’ll notice in the scrolling animation.

Measuring the impact

Now that you’ve identified where the issue is, how about measuring the impact directly from your app? You’ll show it among the other information in the collection.

let startTime = mach_absolute_time()
func mark() -> Int {
  var baseInfo = mach_timebase_info_data_t(numer: 0, denom: 0)

  guard mach_timebase_info(&baseInfo) == KERN_SUCCESS else {
    return -1
  }
  
  let finishTime = mach_absolute_time()
  let nano = (finishTime - startTime) * 
    UInt64(baseInfo.numer / baseInfo.denom)

  return Int(nano / 1000)
}
let timer = MachineTimer()
cell.time = "\(timer.mark())μs"

Solving the problem

Your app was saving new information to the file every time you generated a new number. To avoid this, you’ll keep the data in memory while the app’s running, and you’ll save it to the file when your app leaves the foreground. In other words, you’ll:

func applicationDidEnterBackground(_ application: UIApplication) {
  TrackedNumbersGenerator.saveTrackedNumbers()
}

Core Data

C-1: Faults Optimization lists all the countries in the world and the continent where each resides. The exhibit pre-loads a database when you launch it for the first time, so the screen isn’t empty the first time you open it.

Core Data pre-fetching

Core Data offers a way to pre-fetch objects in relationships within the fetch request. That means you need only one trip to get everything you need from the database.

request.relationshipKeyPathsForPrefetching = 
  ["languages", "continent"]

func clearMemory() {
  context.refreshAllObjects()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
  CoreDataManager.shared.clearMemory()
}

Build times

In this section, you’ll learn about optimizing the code for yourself! Yes, that’s right. You can reduce the time you spend waiting for your app to build.

-Xfrontend -debug-time-function-bodies

-Xfrontend -debug-time-expression-type-checking

-Xfrontend -warn-long-expression-type-checking=25
-Xfrontend -warn-long-function-bodies=25

Chaining & build times

Take those warnings on one by one. Open SlowArray.swift. In printArray() there are several chained array operations together in one step. These operations can be broken down into multiple steps:

func printArray() {
  var doubleArray = Array(repeating: 1.123, count: 100)
  doubleArray = doubleArray.map { $0 * Double.random(in: 0 ..< 1000) }
  var intArray = Array(repeating: 1, count: 100)
  intArray = intArray.map { $0 * Int.random(in: 0 ..< 1000) }
  let doubleMultiply = zip(doubleArray, intArray)
    .map { $0 * Double($1) }
  let sum = doubleMultiply.sorted()
    .map { $0 * 123 }
    .reduce(0, +)
  print(doubleMultiply)
  print(sum)
}
func calculatePoint() -> CGPoint {
  CGPoint(
    x: (UIApplication.shared.windows.first?.frame.size.width ?? 300 / 3)
      + CGFloat.random(in: 0...1000) / CGFloat(100),
    y: (UIApplication.shared.windows.first?.frame.size.height ?? 300 / 3)
      + CGFloat.random(in: 0...1000) / CGFloat(100)
  )
}

func calculateEquation() -> Double {
  (Bool.random() ?
    (pow(pow(Double.random(in: 100...1000), 2.0), 6.0) / 5.5
      + Double.random(in: 100...1000)) * 25 / 3
    + Double.random(in: 100...1000)
    :
    (pow(pow(Double.random(in: 1...100), 2.0), 6.0) / 5.5
      + Double.random(in: 1...100)) * 25 / 3 + Double.random(in: 1...100))
    + Double(UIApplication.shared.windows.first?.frame.size.width ?? 
      CGFloat(400) / 2 * 500 * CGFloat.random(in: 100...1000))
}
static func getSuspiciousStruct() -> Self {
  SuspiciousStruct()
    .setting(\.name) { "SomeName" }
    .setting(\.phone) { "0123456789" }
    .setting(\.email) { "email@somewhere.com" }
    .setting(\.country) { "Earth-Country" }
    .setting(\.city) { "Earth-Country-City" }
    .setting(\.address) { "A place on earth, beside that shop" }
    .setting(\.job) { "Super-Duper iOS Developer" }
}

Timelane

Tracking the progress of asynchronous code is usually not a trivial feat. To help with that, Marin Todorov developed a fantastic instrument called Timelane. You can download it from: http://timelane.tools.

Putting Timelane to work

Build and run the project and look at the console log in Xcode. It shows a lot of started logs, a lot more progress and completed — but it’s very hard to track them. Timelane does that magic for you.

import TimelaneCore
var galleryLane = Timelane.Subscription(name: "Gallery")
// for .started
galleryLane.begin()

// for .cancelled
galleryLane.end(state: .cancelled)

// for .completed
galleryLane.end(state: .completed)

Multiple lanes

Replace the lane you defined at the beginning of the class with a dictionary of lanes:

var galleryLanes: [ImageTask: Timelane.Subscription] = [:]
let imageName = imageTask.request.urlRequest.url?.lastPathComponent ?? ""

switch event {
case .started:
  let lane = Timelane.Subscription(name: "Request " + imageName)
  lane.begin()
  galleryLanes[imageTask] = lane
  print("started " + imageName)
case .cancelled:
  let lane = galleryLanes[imageTask]
  lane?.end(state: .cancelled)
  galleryLanes[imageTask] = nil
  print("canceled " + imageName)
case .completed(result: _):
  let lane = galleryLanes[imageTask]
  lane?.end(state: .completed)
  galleryLanes[imageTask] = nil
  print("completed " + imageName)
case .progressUpdated(
      completedUnitCount: let completed,
      totalUnitCount: let total
):
  let lane = galleryLanes[imageTask]
  let percent = completed * 100 / total
  lane?.event(value: .value("progress: \(percent)"))
  print("progress for \(imageName): \(percent)")
default:
  print("default")
}

TimelaneCombine

Open PhotoViewController.swift and import TimelaneCombine:

import TimelaneCombine
let resizedImagePublisher =
  ImagePipeline.shared.imagePublisher(with: resizedImageRequest)
  .lane("Resized Image")
let originalImagePublisher =
  ImagePipeline.shared.imagePublisher(with: url)
  .lane("Full Image")

Signpost

Now that you saw a custom instrument, explore what made it possible and how the events are logged from your app to instruments.

import os.signpost
let log = OSLog(
  subsystem: "com.raywenderlich.Far-Out-Photos",
  category: "PhotoGallery")

Tracking image downloads

Similar to what you did with Timelane earlier, you want to track the image downloads. Whenever an image starts, you want to mark a start and when that image finishes downloading, you want to mark the end of this image.

os_signpost(.begin, log: log, name: "ImageDownload")
os_signpost(.end, log: log, name: "ImageDownload")

let signpostID = OSSignpostID(log: log, object: imageTask)
os_signpost(.begin, log: log, name: "ImageDownload", signpostID: signpostID)
.
.
.
os_signpost(.end, log: log, name: "ImageDownload", signpostID: signpostID)

Displaying more information

Each signpost can have metadata attached to it. For now, it’s enough to include the name of the image in the begin signpost. Update the begin call to the following:

os_signpost(
  .begin,
  log: log,
  name: "ImageDownload",
  signpostID: signpostID,
  "%{public}s",
  imageName)

os_signpost(
  .end,
  log: log,
  name: "ImageDownload",
  signpostID: signpostID,
  "completed")
os_signpost(.end,
log: log,
name: "ImageDownload",
signpostID: signpostID,
"canceled")

Using signposts as events

Signposts can also be an event — they don’t need to always represent the beginning and the end of an operation. You can have a .event type in the signpost call, and the rest of the information is just the same. However, no matches occur because this event is a standalone signpost. Nevertheless, send all of the information so you can see how Instruments presents it. Add a new signpost call in .progressUpdated, right after the percentage calculation:

os_signpost(.event, log: log, name: "ImageProgress", signpostID: signpostID, "%{public}s progress: percentd", imageName, percent)

Key points

In this chapter, you learned a lot about some of the tools available to understand and measure what’s happening inside your app. You learned how to:

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 accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now