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)
}
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
- Xcode → Product → Profile (Cmd+I)
- Choose Blank template
- Click + → add os_signpost instrument
- Optionally add Points of Interest instrument
- Run the app — your signposts appear as intervals and events
- Filter by subsystem/category to find your custom traces
Instruments Templates
| Template | Best For |
|---|
| Time Profiler | CPU sampling (which functions are slow) |
| os_signpost | Your custom intervals and events |
| Points of Interest | Key moments (user actions, cache hits) |
| Animation Hitches | Frame drops and hitches (iOS 15+) |
| App Launch | Startup time breakdown |
Quick Reference
| API | Availability | Begin | End | Event |
|---|
OSSignposter | iOS 15+ | beginInterval(_:id:) → state | endInterval(_:_:) | emitEvent(_:id:) |
os_signpost | iOS 12+ | os_signpost(.begin, ...) | os_signpost(.end, ...) | os_signpost(.event, ...) |
XCTOSSignpostMetric | Xcode 13+ | Automatic from XCTest | Automatic | N/A |
Key Differences from Android
| Concept | Android (Perfetto) | iOS (Instruments) |
|---|
| Custom trace section | Trace.beginSection("name") | signposter.beginInterval("name", id:) |
| End trace | Trace.endSection() | signposter.endInterval("name", state) |
| Async/concurrent | Trace.beginAsyncSection | OSSignpostID for overlapping intervals |
| Counter | Trace.setCounter("name", value) | Custom Instruments package |
| Viewer | Perfetto UI / Android Studio | Instruments.app |
| Trace format | .perfetto-trace | .trace |
Sources