os_signpost a Instruments v iOS 26: Kompletný sprievodca profilovaním výkonu Swift aplikácií

Praktický sprievodca profilovaním Swift aplikácií pomocou os_signpost, OSSignposter a Instruments v Xcode 26. Interval eventy, Points of Interest, MetricKit a ako neurobiť z traceov nečitateľný chaos.

os_signpost & Instruments v iOS 26 (2026)

Aktualizované: 5. augusta 2026

os_signpost je systémové API pre nízko-nákladové časovanie kritických úsekov Swift kódu, ktoré sa v Instruments zobrazujú ako vizuálne intervaly na časovej osi. V Xcode 26 sa pracuje najmä cez triedu OSSignposter, ktorá nahrádza staršie C-štýlové makrá a integruje sa priamo s Points of Interest, Time Profiler a vlastnými Instruments šablónami. Tento sprievodca ukazuje, ako signposts nasadiť v produkčnej aplikácii tak, aby vám dali reálny signál, nie ďalších tisíc nečitateľných riadkov v traceoch.

  • OSSignposter (Swift-native API dostupné od iOS 15) je preferovaná cesta oproti C makru os_signpost. Má bezpečnejší lifecycle cez SignpostState.
  • Interval eventy (beginInterval/endInterval) merajú trvanie; point eventy (emitEvent) označujú okamžité udalosti ako user tap alebo cache hit.
  • V Instruments 26 sa signposts objavia v Points of Interest a v os_signpost nástroji; kombinujte ich s Time Profilerom pre kauzálne prepojenie intervalu a CPU stacku.
  • Metadata (kľúč-hodnota) prilepené k intervalu sú filtrovateľné v Instruments. Používajte ich pre userId, cacheHit, rowCount namiesto konkatenácie do názvu.
  • Pre produkčné metriky spárujte signposts s MetricKit. Instruments je na dev-loop, MetricKit zbiera od reálnych používateľov.
  • Vytvorte si rozpočet signpostov: ideálne 5 až 15 kritických oblastí. Viac znamená, že tracy sa stanú nečitateľné a signal-to-noise klesne pod hranicu použiteľnosti.

Prečo signposts a nie print alebo os_log

Prvá vec, ktorú robí každý iOS engineer pri hľadaní pomalého kódu, je pridanie print volaní s časovými značkami. Funguje to na tri riadky, ale rozpadne sa vo chvíli, keď má aplikácia asynchrónny fetch, background prácu a UI update v jednom flow. os_log je krok nahor (dostane sa do Console.app a má úrovne), ale stále nedokáže vizuálne ukázať, koľko trval konkrétny úsek kódu vzhľadom na ostatné.

Honestly, signposts sú navrhnuté presne na tento use case. Vygenerujú udalosti do systémového log streamu s minimálnou réžiou (rádovo desiatky nanosekúnd na volanie) a Instruments ich potom vykreslí ako farebné pruhy na časovej osi. Vedľa nich vidíte CPU stacky, sieťovú aktivitu, hlavnú frontu a všetko ostatné, čo štandardné šablóny zbierajú. V mojom Spotify období sme mali signposty okolo dekódovania obalov, prehrávania a lazy-load audia; keď sa Now Playing lagoval, prvý pohľad na trace okamžite ukázal, kde sedeli sekundové medzery.

Praktický rozdiel: print vám povie „tento riadok sa vykonal", os_log povie „vykonal sa a mám ho v logu", ale OSSignposter povie „vykonal sa, trval 380 ms, prekrýval sa s týmto sieťovým requestom a hlavná fronta bola v tom čase 92 % vyťažená". Rozdiel v hustote informácie je zásadný.

OSSignposter: základná integrácia v Swift

Základná integrácia je trojriadková. Vytvoríte OSSignposter pripojený k subsystému a kategórii, otvoríte interval a keď skončíte, zavriete ho. Kategória sa v Instruments premietne na farebnú stopu, ktorú viete filtrovať.

import os.signpost

// Vytvorte raz, najlepšie ako static property na relevantnom type
enum Signposts {
    static let feed = OSSignposter(subsystem: "dev.swiftcrafted.player", category: "Feed")
    static let audio = OSSignposter(subsystem: "dev.swiftcrafted.player", category: "Audio")
}

final class FeedLoader {
    func loadHomeFeed() async throws -> [Track] {
        let state = Signposts.feed.beginInterval("loadHomeFeed")
        defer { Signposts.feed.endInterval("loadHomeFeed", state) }

        let dtos = try await api.fetchFeed()
        return dtos.map(Track.init(dto:))
    }
}

Všimnite si dva detaily. Po prvé, beginInterval vracia opaque hodnotu typu OSSignpostIntervalState, ktorú musíte odovzdať do endInterval. Kompilátor vás na to neupozorní. Zabudnuté volanie ticho stiahne pruh z timeline a diagnostika sa stratí. Vždy používajte defer.

Po druhé, subsystém a kategória by mali byť konzistentné naprieč aplikáciou. Odporúčam mať jeden enum Signposts so statickými inštanciami: vyhnete sa preklepom v stringoch a máte jedno miesto, kde vidíte, koľko oblastí aplikácia sleduje. Ak sa vám tento zoznam natiahne nad 20 položiek, začnite konsolidovať; príliš detailné signposty urobia z traceu obrázkovú knihu.

Ako je to s async funkciami a taskami

V modernom Swift kóde s async/await a štruktúrovanou súbežnosťou je pridanie signpostu okolo await volania stále bezpečné. OSSignposter nedrží žiadny mutable stav medzi begin a end, iba vytvorí unikátne ID. Znamená to, že interval môže presahovať suspension pointy bez toho, aby narušil signposts pipeline.

Interval eventy vs point eventy

Signposts majú dva typy udalostí a používatelia ich často miešajú. Interval má začiatok a koniec a v Instruments sa zobrazí ako obdĺžnik zaberajúci trvanie. Point event je okamih, v Instruments jedna zvislá čiarka na časovej osi.

// Interval: pre operácie s trvaním
let state = Signposts.audio.beginInterval("decodeChunk", id: signposter.makeSignpostID())
// ... práca ...
Signposts.audio.endInterval("decodeChunk", state)

// Point event: pre okamžité udalosti
Signposts.audio.emitEvent("cacheHit", "url: \(url.lastPathComponent)")
Signposts.audio.emitEvent("bufferUnderrun", "queueDepth: \(depth)")

Empirické pravidlo: ak by ste sa pýtali „ako dlho toto trvalo?", použite interval. Ak sa pýtate „stalo sa toto vôbec, a ak áno, kedy?", použite point event. Buffer underruns, cache misses, user taps a retry attempts sú typické point eventy. Sťahovanie súboru, dekódovanie obrázka, render frameu, zápis do SwiftData kontextu sú intervaly.

Prekrývajúce sa intervaly a signpost ID

Ak môžete mať dve inštancie tej istej operácie súbežne (napr. dva paralelné image downloadery), musíte odovzdať explicitné signpostID. Inak Instruments nevie, ktorý end patrí ktorému begin.

func downloadImage(_ url: URL) async throws -> Data {
    let id = Signposts.feed.makeSignpostID()
    let state = Signposts.feed.beginInterval("downloadImage", id: id, "\(url.host ?? "?")")
    defer { Signposts.feed.endInterval("downloadImage", state) }

    return try await session.data(from: url).0
}

Metadata a formátovanie správ

Signposts akceptujú StaticString ako názov a formátované argumenty ako metadata. Instruments dokáže filtrovať a agregovať podľa týchto metadát, čo je jediná cesta, ako z traceu urobiť analytický nástroj namiesto zbierky obrázkov.

let state = Signposts.feed.beginInterval(
    "renderRow",
    "index: \(index, privacy: .public), type: \(row.type, privacy: .public), cached: \(isCached, privacy: .public)"
)

Špecifikátor privacy: .public je dôležitý. Bez neho Instruments zobrazí hodnotu ako <private>. Pre user data, tokeny alebo emaily nechajte default (.private) alebo .sensitive. Metadata musia byť interpolované cez OSLogMessage. Nesnažte sa vopred konkatenovať do stringu, réžia vzrastie a stratíte možnosť filtrovania.

Instruments 26: workflow od záznamu po analýzu

Postup, ktorý používam pre každý perf incident, má šesť krokov. Xcode 26 zjednodušil hlavne krok 1 a 5.

  1. Product → Profile (⌘I) v Xcode. Xcode zbuildne release konfiguráciu s debug symbolmi, zavesí ich na testovacie zariadenie (reálne zariadenie, nie simulátor; CPU charakteristika je iná) a otvorí Instruments.
  2. Vyberte šablónu Time Profiler. Automaticky obsahuje aj Points of Interest inštrument, ktorý vaše signposts zobrazí.
  3. Nahrávajte iba to, čo potrebujete. Optimum je 10–30 sekúnd. Dlhšie záznamy sú takmer nepoužiteľné: trace súbory prekročia gigabajt a UI Instruments začne reagovať sekundy.
  4. Zastavte záznam a začnite od Points of Interest. Nájdete tam všetky vaše signposts oblasti. Kliknutím na interval sa v spodnom paneli zobrazí presné trvanie, metadata a čo bežalo v tom čase.
  5. Cross-referenčne otvorte Heaviest Stack Trace pre časový rozsah vášho intervalu (drag-select na časovej osi). Toto vám dá reálne funkcie, ktoré strávili CPU čas.
  6. Exportujte trace ako .trace package a priložte do bug reportu. Trace vie otvoriť ktorýkoľvek kolega bez toho, aby musel reprodukovať scenár.

Vlastné Instruments šablóny

Pre opakované incidenty sa oplatí vytvoriť package: vlastný Instruments dokument s vopred nakonfigurovaným setom nástrojov a filterov. V praxi to znamená menej klikania medzi releasmi a konzistentný pohľad naprieč tímom. Trvalo mi to raz nastaviť, odvtedy je to jeden double-click pri každom perf review.

Kombinovanie s Time Profilerom a System Trace

Signposts samotné vám povedia „táto operácia trvala 240 ms". Time Profiler pridá „a strávila to v týchto funkciách". System Trace ide ešte hlbšie a ukáže syscalls, context switches, thread state a I/O, čo je nevyhnutné, ak podozrievate lock contention alebo priority inversion.

Kombinácia, ktorú používam najčastejšie na diagnostikovanie „záhadných" spomalení:

  • Signposts označia oblasť záujmu.
  • Time Profiler ukáže CPU využitie a hot funkcie počas intervalu.
  • System Trace ukáže, či thread naozaj bežal alebo bol zablokovaný na semafóre, IO, mutex-e.
  • Thread State stĺpec často odhalí, že vlákno bolo runnable ale nebežalo, čo je typicky priority inversion.

Ak profilujete aplikáciu, ktorá komunikuje cez sieť, spárujte to s článkom o modernom URLSession klientovi. Signposty okolo URLSession volaní dokonale ukážu, koľko času sa strávi čakaním na server vs. parsovaním a mapovaním DTO-čiek.

MetricKit: signposts z reálnych zariadení

Instruments funguje pre developerský cyklus a QA. Pre produkčné dáta z reálnych zariadení potrebujete MetricKit. Ten agreguje metriky výkonu (launch time, hang durations, CPU usage, disk writes) a raz denne ich doručí do aplikácie ako MXMetricPayload.

Kombinácia so signposts prichádza cez MXSignpostRecord a MXSignpostMetric. MetricKit vie agregovať trvanie a percentilové rozdelenia intervalov, ktoré emitujete cez OSSignposter s kategóriou PointsOfInterest.

import MetricKit

final class MetricsSubscriber: NSObject, MXMetricManagerSubscriber {
    override init() {
        super.init()
        MXMetricManager.shared.add(self)
    }

    func didReceive(_ payloads: [MXMetricPayload]) {
        for payload in payloads {
            guard let signpost = payload.signpostMetrics else { continue }
            for metric in signpost {
                let name = metric.signpostName
                let hist = metric.histogrammedSignpostDurations
                // Odoslať do vášho analytics pipelinu
                analytics.record(perfHistogram: hist, name: name)
            }
        }
    }
}

V praxi majte malú podmnožinu signpostov (5–8), ktoré chcete sledovať v produkcii. Nie všetko, čo profilujete v Instruments, musí ísť do MetricKit. Filtrujte podľa toho, čo skutočne používate pri triage v dashboardoch. Podľa oficiálnej dokumentácie systém vzorkuje signposts aj v release buildoch tak, aby réžia zostala neviditeľná: čísla, ktoré dostanete cez MetricKit, sú štatisticky spoľahlivé.

Časté chyby, ktorým sa vyhnúť

1. Zabudnuté endInterval

Najčastejší bug. Intervaly bez end sa v Instruments objavia ako otvorené pruhy siahajúce do konca traceu, čo skresľuje analýzu. Vždy používajte defer hneď po beginInterval, aj v jednoduchých funkciách. Ja som na túto pascu naletel v jednom side projekte a dva dni som pátral po „zaseknutej" fronte, kým ma niekto na code review upozornil, že to je nezavretý interval.

2. Signposty vo vnútri tight loopov

Réžia jedného beginInterval je desiatky nanosekúnd. Ak ich vložíte do slučky, ktorá beží milión-krát za sekundu, začnete meniť to, čo meriate. Signposty patria na úroveň sémantických operácií (načítanie stránky, dekódovanie snímky, render riadku), nie na úroveň individuálnych iterácií.

3. Konkatenácia namiesto interpolácie

Zápis signposter.beginInterval("query \(sql)") vytvorí nový string pri každom volaní. Správna forma je signposter.beginInterval("query", "sql: \(sql, privacy: .public)"). Názov ostane StaticString a hodnota sa serializuje lazy.

4. Testovanie na simulátore

Simulator beží na výkonnom Mac CPU s inou pamäťovou hierarchiou. Časovania z neho sú irelevantné pre reálne zariadenie. Vždy profilujte na fyzickom telefóne, ideálne na najslabšom podporovanom modeli. Ak Swift Testing testy bežia na simulátore, perf checky do nich neposielajte. Dajte ich do samostatnej scheme, ktorá sa spúšťa na device farme.

5. Príliš veľa signpostov

Videl som codebasy s 200+ signpostmi. Trace bol nečitateľný, filter cyklus dlhší ako samotný diagnostický cyklus. Držte sa 5–15 dôležitých oblastí a používajte metadata na rozlíšenie inštancií. Ak máte pocit, že potrebujete viac, prehodnoťte, či to nie je znak, že by váš kód mohol byť lepšie štruktúrovaný.

Časté otázky

Aký je rozdiel medzi os_log a os_signpost?

os_log zapisuje textové správy do systémového logu na diagnostiku (chyby, upozornenia, info). os_signpost zapisuje časové udalosti pre výkonnostnú analýzu. V Instruments sa zobrazia ako intervaly na časovej osi, čo umožňuje merať trvanie a koreláciu s CPU stackmi.

Fungujú signposts v release buildoch?

Áno. OSSignposter je navrhnutý s minimálnou réžiou (desiatky nanosekúnd na volanie) a systém vzorkuje udalosti tak, aby nemali detekovateľný vplyv na výkon. Bezpečne ich môžete nechať v produkcii pre neskorší profiling a zber cez MetricKit.

Ako profilovať iOS aplikáciu s Instruments?

V Xcode stlačte ⌘I (Product → Profile). Xcode zbuildne release verziu a otvorí Instruments. Vyberte šablónu Time Profiler, spustite záznam na reálnom zariadení, vykonajte scenár (10–30 sekúnd) a zastavte. Vaše signposts nájdete v paneli Points of Interest.

Prečo sa metadata v Instruments zobrazujú ako <private>?

Systém logovania štandardne označí dynamické hodnoty ako súkromné. Ak sú to non-sensitive dáta ako indexy, typy alebo boolean flagy, pridajte špecifikátor privacy: .public do interpolácie, napríklad "index: \(i, privacy: .public)".

Môžem používať OSSignposter v async funkciách?

Áno, plne. OSSignposter nedrží žiadny mutable state medzi beginInterval a endInterval. Intervalové ID sa odovzdáva cez opaque OSSignpostIntervalState hodnotu, ktorá bezpečne prekročí await suspension pointy.

O Autorovi Daniel Okafor

Daniel is a former Spotify iOS engineer (2019-2024) who worked on the Now Playing surface and the in-app podcast player. He shipped the SwiftUI rewrite of the lyrics view to over 600 million users and contributed several fixes upstream to swift-collections. His writing tends toward the unglamorous corners of iOS work: build-time regressions in Xcode 16, why SwiftData still isn't ready for production sync scenarios, and how to instrument a real app with os_signpost without drowning in noise. He spent two years before Spotify at a fintech startup in Berlin building a banking app on top of Solaris API. Daniel now freelances out of Lisbon and maintains a small open-source library for type-safe deep links in SwiftUI. He has 9 years of native iOS experience.