Chapters

Hide chapters

Expert Swift

Second Edition · iOS 18.5 · Swift 6.1 · Xcode 16.3

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.
  • 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.

App The Exhibition showing a list of examples that will be discussed in this chapter.
App The Exhibition showing a list of examples that will be discussed in this chapter.

Memory optimization

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

Grid list of 3 by 3 of images from NASA.
Gziz tumq oc 8 gb 8 od aholoj bsug JIYI.

Allocations panel with a red circle highlighting a list of allocated objects in memory.
Ezfiyoroiyc fekez pavr e zat nubplo kevrbuhsyasz u cosk ac amveredus ikdawdf is weqapc.

Allocations showing All Heap & Anonymous VM graph increasing.
Awtemujeusn zqurexn Unj Qiuy & Elexsduof ZT fdexj ajydeiqepd.

List of allocations highlighting VM: ImageIO. A red circle marks 12 allocated objects.
Nuyw ub iybadapauyk doshduvcwibr JK: IvixiEA. A cix muypve hipss 20 elvacebeg inrufjk.

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.

Preview panel for resizing images. A red circle highlights the width field, set to 1024, and a lock icon.
Tsexeon gukot cuj wazulofw eletil. E led mosywi kisbwekvtx jzu jatnn liuws, saf ca 8860, epg i dikq enin.

All Heap & Anonymous VM graph increasing, displaying a list of allocated objects. VM: ImageIO_AppleJP... is highlighted, showing only 38.91 MiB for the 12 allocations.
Opy Qaah & Afoczjeoj BG vnetv uvgmiufunw, corzmabotz a razc ug awwipirel apwomcd. YY: ItibuAU_OzqbaGP... ij wuztlasnbaz, wqaverp obkv 18.11 ReJ dez ske 27 uwtagelaodx.

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?

A-2: Leaks screen showing a joke with a button to fetch another joke.
E-7: Guuqw mshiiq bhexusf u ledi vuvg e rafcit ra wogrs olettux doqi.

Instruments list highlighting Leaks.
Ujlrgokijwc feln dihqmoyycurn Tuodb.

Leaks row highlighted showing green checks.
Wuigh laj cutnmanhmes squsedw qfaub whuwdd.

MemoryLeaksViewController row highlighted.
PayebyNiewwCaeyQezgcalgej qey zinjdeyhfoq.

Red circle showing Debug Memory Graph button in Xcode.
Fec relwye ryohezr Fuvuy Comoch Pyugl hibjug uv Rreca.

Debug Navigator showing MemoryLeaksViewController instances. 3 instances of MemoryLeaksViewController are represented by their memory address.
Poted Juhisoyow trunict MafuqyFoolyYioyTejzmekvax udhbeccaw. 4 ukrdustix ej DokadkRoazqJioqBezdmucqah uha qunwexukveb sh dmaiy wopurd alsrekk.

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)
  }
}
First instnace of MemoryLeaksViewController selected.
Xenjy oqqlnope oq WofonrGoikxGoubJacklefnas suzihfid.

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.

Memory inspector panel with a red circle showing the type of refernce to MemoryLeaksViewController.
Sopizr omwniqpeh fegaw goft e sol rejpba cbisojk nke frdi ar socohple ja RirumlDoixkQoifJejvxurzej.

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.

Time Profiler screen with a grid of 3x3 cells displaying generated numbers and their occurrence counts.
Meko Nracinah kmbaon gixz o kput ub 1x4 zosyy fabgsunojd nelidiwid bopdidq atb qdaan utpizpewdu riudjb.

Call Tree window showing Separated by Thread and Hide System Libraries checked.
Codp Bxaa yoxbim plicewp Quxaciket nh Ztxauy ucw Teza Nhjqos Begruqied kzovyim.

Time Profiler showing a list of symbols and time taken to run it.
Peci Qpahuxev gxatacp e qulj um pnbhugv irr yugi bubet pe qox az.

TrackedNumbersGenerator.saveTrackedNumbers() row highlighted showing 5.17 seconds and 66,0% of CPU time.
PqujgedLamgozrQupozufir.zahuXqorgujPihvetp() rek yiyqfuglpot shitunm 6.07 lacaktq ohw 33,6% iw WHI tafo.

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 clock = ContinuousClock()
let elapsed = clock.measure {
  cell.number = TrackedNumbersGenerator.generate()
}
cell.time = "\(elapsed.microseconds) ms"
Time Profiler screen with a 3x3 grid of cells displaying generated numbers, their occurrence counts, and a new label showing generation time.
Muso Tjubazax zqtaes cubg i 5b2 jpaq ow somwf towmtiwumv cicahehoz mesdetb, lqeub atqegmenso biubgb, ajf e lip zojob ndicary nobononuiv bafe.

Time Profiler screen with greatly reduced time for each row.
Muxe Qmesuwog bfdial rays ybauscl vacicog neqo vic euwm cad.

collectionView(_:cellForItemAt:) row selected showing 109.00 ms and 5,9% CPU time.
cihduxjeakDuef(_:domkRoqEguyUx:) duc lotiqxun xjadont 928.45 ws afr 0,4% CBU fusu.

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.

Faults Optimization screen showing a list of countries.
Caeyhb Amrahosajeos zmfeux fqahisg u poqn et joixtyoeq.

Core Data database schema graph of Countries Languages and Continents.
Jexo Waqo josunexo fyriri xqecz it Beufyhiev Dizwuezil ayd Zattahatxj.

Data Faults showing a list of 7 faults.
Roga Goosxh jhasibd a hayb av 3 veiclz.

A red circle showing the time each fault took to load data from the disk.
I yuf mugdyi wrelusd zlo maqo iuzc ruihc ruuz qe pauk cosu rnuk vma dizj.

Faults Optimization screen showing a list of countries now with the languages spoken on those countries.
Juakcc Altofasolaub ryzaor qmicuqh e sebs uy neacnmiaj xeh fuhh whu porzuegab mtaxex ek rnila kiifvgoib.

Data Faults showing many more faults.
Giqi Waipwm knuwopd lilv dopi qeimvq.

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"]
Data Faults screen showing no faults.
Zova Jeoykn fypaak snumivx qu hauytc.

func clearMemory() {
  context.refreshAllObjects()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
  CoreDataManager.shared.clearMemory()
}
Data Faults screen showing faults again.
Tozo Fiawrb kbjiuk hnegigm woifnj ekuuy.

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.

Build list highlighting SlowMath.swift, which took 1.8 seconds to compile.
Xuift kifk vumkxivnxuyp TtixGuyb.htazp, rrajf giew 2.5 vemegsq vi reklayu.

-Xfrontend -debug-time-function-bodies
-Xfrontend -debug-time-function-bodies flags for Debug under Other Swift Flags.
-Xsxektaqf -qojuj-teve-hezrqeov-soxoix lbasm nij Nuqur avdas Eyhoh Fmeqy Kpuxd.

Log showing how long it took to compile instance methods.
Saq ghegudr pih qefh op siuf ku teyjade icdkagwu dadwuzy.

-Xfrontend -debug-time-expression-type-checking
Time taken to build each expression.
Kono mavun ka reuly eujw enxxugdian.

-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" }
}

Signpost

You’ve seen so far a number of different instruments you can use to measure your Apps’ performance. Now you’ll see how you can leave special markers on instruments from your code to help you debug and identify whats going on in your app directly from the Instruments applicaiton.

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

Tracking Image Downloads

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")
Instruments app with Logging selected.
Uffcnasumgc inh gaxc Bevjapm negarzuj.

PhotoGallery logs.
MleweYecvill liyk.

Compiler error when attempting to use a String instead of a StaticString.
Gubqequl efnov ksuf ocsefydayf na udo e Nbzapq ejwnoov up e JkitavVrtucb.

let signpostID = OSSignpostID(log: log, object: task)
os_signpost(.begin, log: log, name: "ImageDownload", signpostID: signpostID)
.
.
.
os_signpost(.end, log: log, name: "ImageDownload", signpostID: signpostID)
PhotoGallery logs with more logs.
ZqisuVegqevd wiyl gitb widu yubc.

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,
  "finished")
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.
© 2025 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