Using Swift Signpost to Measure Performance of a Specific Function

Chase
5 min readJan 1, 2024

--

Ever wondered how long it takes a single function to run, or how many times it ran? There is a built in method in Swift that doesn’t require you to use any math skills or complex calculations. Let’s get started.

The text of Signpost between the post of two signs

Before we get started, please take a couple seconds to follow me and 👏 clap for the article so that we can help more people learn about this useful content.

Initial Setup

The code that we are working with in this tutorial came from this article, I will also add the code below. If you aren’t very familiar with the Instruments app, feel free to click here (it isn’t required, just a suggestion) to learn everything you will want to know before reading this article.

//  ContentView.swift
import SwiftUI

struct ContentView: View {
@State private var lightSwitch = false

// Using a dictionary here, we have made a basic example
// of how to use memoization in Swift
@State private var fibonacciMemo = [1: 1, 2: 1]

var body: some View {
VStack {
Image(systemName: "lightbulb.fill")
.resizable()
.scaledToFit()
.frame(width: 100, height: 100)
.padding(.bottom)
.foregroundStyle(lightSwitch ? .yellow : .black)
Button(lightSwitch ? "Turn Off" : "Turn On") {
Task {
await fetchLightState()
}
}.buttonStyle(.borderedProminent)
}
.padding()
.task {
await fetchLightState()
}
}

func fetchLightState() async {
let _ = await betterFibonacci(45)
lightSwitch.toggle()
}

func betterFibonacci(_ n: Int) async -> Int {
// check if the key is in the dictionary
if fibonacciMemo[n] != nil { return fibonacciMemo[n]! }

fibonacciMemo[n] = await betterFibonacci(n - 1) + betterFibonacci(n - 2)

return fibonacciMemo[n]!
}
}

#Preview {
ContentView()
}

When we measure the performance of the code above in the Instruments app, you should see a chart similar to the following.

We can see that the graph doesn’t have much usage except right after app launch, and not much after that. Sometimes either when trying to debug performance or just trying to get more granular in your measurements, you may want to know how to measure the performance of a specific function, or to find out how many times that function ran. In Swift the way to do that is by using a Signpost with the Instruments app.

Using Signpost in Swift

A signpost is a way for Swift to convert a log into a usable/measurable and really helpful tool in using the Instruments app. Many developers will place print statements and breakpoints all over their code to manually check for similar details. Don’t get me wrong, those options still work well and still have their place, Signposts are another tool that you can add to your performance/debugging engineering tool belt.

Let’s add a signpost to our betterFibonacci function to see when it was called, how long it took to run, and how many times it was called. Adding that signpost to our code looks like the following.

//  ContentView.swift
import SwiftUI
import OSLog

struct ContentView: View {
@State private var lightSwitch = false

// Using a dictionary here, we have made a basic example
// of how to use memoization in Swift
@State private var fibonacciMemo = [1: 1, 2: 1]

var body: some View {
VStack {
Image(systemName: "lightbulb.fill")
.resizable()
.scaledToFit()
.frame(width: 100, height: 100)
.padding(.bottom)
.foregroundStyle(lightSwitch ? .yellow : .black)
Button(lightSwitch ? "Turn Off" : "Turn On") {
Task {
await fetchLightState()
}
}.buttonStyle(.borderedProminent)
}
.padding()
.task {
await fetchLightState()
}
}

func fetchLightState() async {
let _ = await betterFibonacci(45)
lightSwitch.toggle()
}

func betterFibonacci(_ n: Int) async -> Int {
// Create a signposter that uses the default subsystem.
let signposter = OSSignposter()

// Generate a signpost ID to associate with the signposted interval.
let signpostID = signposter.makeSignpostID()

// Create a name that the signposter uses, along with the
// signpost ID, to disambiguate the begin call and end call.
// The type must be StaticString.
let name: StaticString = "betterFibonacci"

// Begin a signposted interval and keep a reference to the
// returned interval state.
let state = signposter.beginInterval(name, id: signpostID)

defer {
// Use the interval state from the begin call to end the
// corresponding signposted interval.
signposter.endInterval(name, state)
}

// check if the key is in the dictionary
if fibonacciMemo[n] != nil { return fibonacciMemo[n]! }

fibonacciMemo[n] = await betterFibonacci(n - 1) + betterFibonacci(n - 2)

return fibonacciMemo[n]!
}
}

#Preview {
ContentView()
}

You may notice that we have added a new import at the top of the file (don’t forget this step). We have also added some extra code to our betterFibonacci function, most of which has been annotated in the comments of the code. The contents of the defer block will be run at the end of the current scope, meaning that whenever the betterFibonacci function is done, the code in the defer block will run. This can help us cover the scenario in our code where we would return early from a function. To summarize what the extra code above is doing, we are creating a signpost instance with a custom name for the function we want to measure, starting the recording, waiting for the betterFibonacci function to end, and stopping the recording, that wasn’t too bad, right?

Measuring Singposts in the Instruments app

The benefit of adding signposts comes when we measure our code using the Instruments app. To see and measure our Signpost, we will launch our code in the Instruments app, and add the os_signpost instrument to the track event list (if you don’t remember how to do that you can learn how here). The Signpost API used to be called os_signpost even though that API has now been deprecated, that is currently still that name of the instrument used to measure a Signpost in the Instruments app. Once we start recording, we can click the button from the sample code a couple of times and then stop the recording, our performance graph should look similar to the following image.

A screenshot of the the Signpost that we added to our code measured by the Instruments app

As we can see from the image above, our function ran a total of 91 times, all of those runs only totaled up to 58 milliseconds, with the longest single run lasting about 2.7 milliseconds. That is a massive improvement over the poorlyImplementedFibonacciCalc function that we were using in this article that took 37 seconds to run!

If you got value from this article, please consider following me, 👏 clapping for this article, or sharing it to help others more easily find it.

If you have any questions on the topic, or know of another way to accomplish the same task, feel free to respond to the post or share it with a friend and get their opinion on it. If you want to learn more about native mobile development, you can check out the other articles I have written here: https://medium.com/@jpmtech. If you want to see apps that have been built with native mobile development, you can check out my apps here: https://jpmtech.io/apps. Thank you for taking the time to check out my work!

--

--