print-based timing tells you one number in the console, on one run, with no way to correlate it against what else the app or system was doing at that moment — no thread state, no overlapping work, no visual timeline.
#The old way
import Foundation
func loadFeed() {
let start = Date()
performExpensiveDecode()
let elapsed = Date().timeIntervalSince(start)
print("loadFeed took \(elapsed)s")
}
#The new way
import os
let logger = Logger(subsystem: "com.example.app", category: "feed")
let signposter = OSSignposter(logger: logger)
func loadFeed() {
let state = signposter.beginInterval("Load Feed")
performExpensiveDecode()
signposter.endInterval("Load Feed", state)
}
#Why it matters
- Signpost intervals show up as named ranges directly on Instruments' timeline, alongside CPU, memory, and thread activity for the same window
OSSignposteris built on the unified logging system (Logger), so the same category and subsystem you use for regular logs group your performance data too- Signposts cost very little at runtime and are safe to leave in release builds — they're inert unless a profiling tool is actually attached
- Nesting intervals lets you see which part of a slow operation is actually expensive, instead of one aggregate number for the whole function
#Gotcha
Signposts only tell a story if attached with Instruments (or a custom os_signpost handler) while the code runs — nothing shows up from a debug build the phone was just carrying around normally, which trips people up expecting some form of always-on logging. Interval names passed to beginInterval/endInterval must match exactly for Instruments to pair them into a single range; a typo produces two orphaned point events instead of one interval. The exact OSSignposter method signatures have shifted slightly since introduction, so confirm parameter labels against current documentation before wiring this into a shared logging helper.