Back to Articles
iOS Tracing and Profiling Guide with OSSignposter

iOS Tracing and Profiling Guide with OSSignposter

1. Modern API: OSSignposter (iOS 15+)

Apple’s recommended API for performance instrumentation. Low overhead, designed for Instruments visualization.

Basic Interval (Measure Duration)

import os

class DataService {
    private let logger = Logger(subsystem: "com.example.app", category: "DataService")
    private let signposter: OSSignposter

    init() {
        signposter = OSSignposter(logger: logger)
    }

    func fetchAndProcessData() async throws -> [Item] {
        let signpostId = signposter.makeSignpostID()
        let state = signposter.beginInterval("fetchAndProcessData", id: signpostId)

        defer {
            signposter.endInterval("fetchAndProcessData", state)
        }

        let data = try await network.fetch()

        // Emit intermediate event within the interval
        signposter.emitEvent("fetchComplete", id: signpostId)

        let items = parse(data)
        return items
    }
}

Convenience: withIntervalSignpost

func loadImage(url: URL) async throws -> UIImage {
    let signposter = OSSignposter(subsystem: "com.example.app", category: "Images")

    return try await signposter.withIntervalSignpost("loadImage") {
        let (data, _) = try await URLSession.shared.data(from: url)
        return UIImage(data: data)!
    }
}

Measuring a Specific Function

func measureSort() {
    let signposter = OSSignposter(subsystem: "com.example.app", category: "Sorting")
    let signpostId = signposter.makeSignpostID()
    let name: StaticString = "sortAlgorithm"

    let state = signposter.beginInterval(name, id: signpostId)
    defer { signposter.endInterval(name, state) }

    // ... sorting work ...
}

Overlapping / Concurrent Operations

// Each operation gets its own signpost ID — Instruments matches begin/end pairs
func downloadFiles(_ urls: [URL]) async {
    let signposter = OSSignposter(subsystem: "com.example.app", category: "Downloads")

    await withTaskGroup(of: Void.self) { group in
        for url in urls {
            group.addTask {
                let id = signposter.makeSignpostID()
                let state = signposter.beginInterval("download", id: id,
                    "\(url.lastPathComponent)")

                defer { signposter.endInterval("download", state) }

                try? await URLSession.shared.data(from: url)
            }
        }
    }
}

Point Events (Points of Interest)

// Shows up in the "Points of Interest" instrument
let poiLog = OSLog(subsystem: "com.example.app", category: .pointsOfInterest)
let signposter = OSSignposter(logHandle: poiLog)

signposter.emitEvent("UserTappedCheckout")

2. Legacy API: os_signpost (iOS 12+)

For apps that need to support iOS 12–14.

Setup

import os.signpost

let osLog = OSLog(subsystem: "com.example.app", category: "Networking")

Interval (Begin / End)

func fetchData() {
    let signpostId = OSSignpostID(log: osLog)

    os_signpost(.begin, log: osLog, name: "fetchData", signpostID: signpostId)

    // ... network request ...

    os_signpost(.end, log: osLog, name: "fetchData", signpostID: signpostId)
}

With Metadata

os_signpost(.begin, log: osLog, name: "fetchData", signpostID: signpostId,
    "URL: %{public}s", url.absoluteString)
os_signpost(.end, log: osLog, name: "fetchData", signpostID: signpostId,
    "Bytes: %d", responseSize)

Event (Single Point in Time)

os_signpost(.event, log: osLog, name: "cacheHit", "key: %{public}s", cacheKey)

Points of Interest

let poiLog = OSLog(subsystem: "com.example.app", category: .pointsOfInterest)
os_signpost(.event, log: poiLog, name: "UserAction", "Tapped checkout button")

3. SwiftUI View Tracing

import os
import SwiftUI

struct ContentView: View {
    private let signposter = OSSignposter(
        subsystem: "com.example.app", category: "SwiftUI"
    )

    var body: some View {
        let _ = signposter.emitEvent("ContentView.body evaluated")

        VStack {
            // ... view content ...
        }
    }
}

// For measuring onAppear / task durations
struct DetailView: View {
    @State private var data: [Item] = []
    private let signposter = OSSignposter(
        subsystem: "com.example.app", category: "DetailView"
    )

    var body: some View {
        List(data) { item in
            Text(item.name)
        }
        .task {
            let id = signposter.makeSignpostID()
            let state = signposter.beginInterval("loadData", id: id)
            defer { signposter.endInterval("loadData", state) }

            data = await fetchItems()
        }
    }
}

4. UIKit View Tracing

class MyTableViewController: UITableViewController {
    private let signposter = OSSignposter(
        subsystem: "com.example.app", category: "TableView"
    )

    override func tableView(_ tableView: UITableView,
                            cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let id = signposter.makeSignpostID()
        let state = signposter.beginInterval("cellForRowAt", id: id,
            "row: \(indexPath.row)")
        defer { signposter.endInterval("cellForRowAt", state) }

        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
        configure(cell, at: indexPath)
        return cell
    }
}

5. Programmatic Trace Capture (XCTest)

import XCTest

class PerformanceTests: XCTestCase {

    func testScrollPerformance() throws {
        let app = XCUIApplication()
        app.launch()

        // Captures an Instruments trace
        let metrics: [XCTMetric] = [
            XCTOSSignpostMetric(subsystem: "com.example.app", category: "TableView",
                                name: "cellForRowAt"),
            XCTClockMetric(),
            XCTCPUMetric(),
            XCTMemoryMetric()
        ]

        measure(metrics: metrics) {
            app.swipeUp()
        }
    }
}

6. Viewing in Instruments

  1. Xcode → Product → Profile (Cmd+I)
  2. Choose Blank template
  3. Click + → add os_signpost instrument
  4. Optionally add Points of Interest instrument
  5. Run the app — your signposts appear as intervals and events
  6. Filter by subsystem/category to find your custom traces

Instruments Templates

TemplateBest For
Time ProfilerCPU sampling (which functions are slow)
os_signpostYour custom intervals and events
Points of InterestKey moments (user actions, cache hits)
Animation HitchesFrame drops and hitches (iOS 15+)
App LaunchStartup time breakdown

Quick Reference

APIAvailabilityBeginEndEvent
OSSignposteriOS 15+beginInterval(_:id:) → stateendInterval(_:_:)emitEvent(_:id:)
os_signpostiOS 12+os_signpost(.begin, ...)os_signpost(.end, ...)os_signpost(.event, ...)
XCTOSSignpostMetricXcode 13+Automatic from XCTestAutomaticN/A

Key Differences from Android

ConceptAndroid (Perfetto)iOS (Instruments)
Custom trace sectionTrace.beginSection("name")signposter.beginInterval("name", id:)
End traceTrace.endSection()signposter.endInterval("name", state)
Async/concurrentTrace.beginAsyncSectionOSSignpostID for overlapping intervals
CounterTrace.setCounter("name", value)Custom Instruments package
ViewerPerfetto UI / Android StudioInstruments.app
Trace format.perfetto-trace.trace

Sources