macOS OSSignposter: Correlate Concurrent Work Without Mispaired Intervals
Instrument overlapping macOS tasks with OSSignposter IDs and interval state, then inspect duration and correlation in Instruments without false pairings.
Signposts turn an application’s meaningful operations into intervals and events on an Instruments timeline. The hard part is not writing a begin and end call; it is keeping them paired when requests overlap, move through asynchronous work, or fail. OSSignposter supplies a signpost ID and an interval state for that purpose. Use those values as part of the operation’s lifecycle rather than as throwaway logging details.
Give each in-flight operation its own identity
Apple’s OSSignpostID documentation says intervals with the same name, subsystem, and category can overlap. An ID disambiguates them. Use .exclusive only when that kind of interval cannot overlap within the signposter’s scope. For concurrent requests, generate a new ID for each invocation, or derive one from an object whose identity genuinely tracks the request.
import os
let signposter = OSSignposter(
subsystem: "com.example.document-app",
category: .pointsOfInterest
)
func renderDocument(_ document: Document) throws {
let id = signposter.makeSignpostID()
let state = signposter.beginInterval("RenderDocument", id: id)
defer { signposter.endInterval("RenderDocument", state) }
try document.render()
}
Here Document and render() stand for application code, not a framework API. defer makes the synchronous interval close when render() throws. The static signpost name keeps the interval label stable in Instruments; the signposter’s initialization sets its category. For asynchronous work, do not end the interval merely because a function returned a task or scheduled a callback. Retain the interval state with the logical operation and end it at actual completion, cancellation, or failure. Decide which event counts as completion before measuring it.
Choose the subsystem as a stable identifier for the application or component, and the category as a coherent measurement area. A single category for every operation produces a cluttered trace; a category dynamically derived from each document or request makes comparisons difficult. Apple groups captured signposts by subsystem and category in Instruments, so the choice affects how a team navigates results. Keep the interval name stable across runs when it represents the same operation.
The ID should represent one invocation, not merely one function name. For example, two documents can render concurrently with the same RenderDocument name. A fresh makeSignpostID() for each render lets Instruments pair the correct begin and end even if completion order reverses. If an object has the same lifetime as the operation, makeSignpostID(from:) can derive an ID from it; verify that the object is not reused for distinct overlapping requests.
Preserve the pairing contract
Apple documents a one-begin/one-end interval contract. endInterval checks that the state is consumed only once, the name matches, and the begin and end use the same underlying log. In debug builds, a violation can cause an assertion failure; in production, an error is attached to the closing signpost instead. Duplicate completion paths can therefore contaminate the trace even when the app appears to keep running.
For a concurrent pipeline, store the state in the request object or another single owner. Arrange cancellation and normal completion so only one path closes it. Use emitEvent for a point in time, such as “response received,” rather than inventing a zero-length interval. If an interval crosses a process boundary, follow Apple’s state-encoding guidance; .exclusive is specifically unsuitable for cross-process intervals.
Separate an operation’s measured duration from its constituent phases. A top-level interval can span “request accepted” through “result delivered,” while nested or adjacent intervals mark decode, rendering, and persistence. This makes time spent waiting visible instead of forcing every delay into a CPU-work bucket. Do not assume nested intervals are automatically a strict call tree: asynchronous tasks can overlap and resume in different orders. IDs and ownership make the trace interpretable when that happens.
For an asynchronous operation, write down an end-state table before instrumentation: success, explicit cancellation, timeout, and failure should each close the interval exactly once. A common bug is to end it in both a cancellation handler and a completion callback that still fires afterward. Another is to end the interval at callback registration, producing an impressive but meaningless microsecond measurement. Unit tests can drive both race orders even when a real Instruments session is needed to review the visual trace.
Measure the operation, not ambient activity
Profile a representative workload from Xcode with Instruments’ os_signposts instrument. Apple says the instrument groups signposts by subsystem and category and exposes metadata in its timeline and detail table. A points-of-interest category helps surface important spans. When comparing runs, use the same named interval as a filter so an unrelated CPU burst does not dominate the comparison. Keep the workload, input size, and build configuration comparable.
Signposts provide timing and correlation, not a complete explanation of why an operation was slow. A long interval may include I/O, waiting, scheduling delay, or work on another queue. Pair the trace with CPU profiling and relevant application logs. Avoid putting sensitive document names or user data into metadata unless the privacy and retention consequences have been reviewed.
For a performance investigation, capture a baseline trace, change one implementation factor, and capture a second trace using comparable inputs. Apple’s CPU profiling guidance recommends filtering both traces to the same signposted operation before comparing call trees. Inspect distributions, not only the fastest sample: a change that improves typical work but creates a long tail may still worsen user experience. If the interval includes queueing, pair it with narrower phase markers before attributing the delay to a specific algorithm.
After adding signposts, run a deliberate overlap test with two operations that finish in reverse order. Confirm in Instruments that each interval has the expected start and end, no error-marked closing signpost, and no unmatched spans. Then exercise cancellation and thrown-error paths. This QA catches pairing defects that a single sequential happy-path trace will never reveal.
Related:
- How to Diagnose macOS Problems with log stream, Predicates, and Signposts
- Unified Logging: How os_log Replaced syslog on macOS
Sources: